<?php
/**
 * Core engine for static HTML generation and early serving.
 * 
 * @package WPStaticHTMLPages
 */

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

class WPSH_Core {
	/**
	 * Check whether WooCommerce is installed/active enough for WC helpers to exist.
	 *
	 * @return bool
	 */
	private function is_woocommerce_active() {
		return ( class_exists( 'WooCommerce' ) || function_exists( 'WC' ) || function_exists( 'wc_get_page_id' ) );
	}

	/**
	 * Get CPT -> listing page URL mapping (stored in a single option).
	 *
	 * @return array<string,string> Associative map: post_type => url/path/page_id
	 */
	public function get_cpt_listing_page_map() {
		$map = get_option( 'wpsh_cpt_listing_page_map', array() );
		return is_array( $map ) ? $map : array();
	}

	/**
	 * Normalize a configured mapping entry into a full URL.
	 *
	 * Supported inputs:
	 * - page ID (numeric string)
	 * - absolute URL (starts with http/https)
	 * - path (starts with /)
	 *
	 * @param string $raw Entry value.
	 * @return string Empty string on invalid.
	 */
	private function normalize_listing_mapping_entry_to_url( $raw ) {
		$raw = trim( (string) $raw );
		if ( '' === $raw ) return '';

		// If it's a page ID, resolve permalink.
		if ( ctype_digit( $raw ) ) {
			$url = get_permalink( (int) $raw );
			return $url && ! is_wp_error( $url ) ? (string) $url : '';
		}

		// Full URL provided.
		if ( 0 === strpos( strtolower( $raw ), 'http://' ) || 0 === strpos( strtolower( $raw ), 'https://' ) ) {
			return esc_url_raw( $raw );
		}

		// Path-only provided.
		if ( 0 === strpos( $raw, '/' ) ) {
			$home = home_url( '/' );
			$raw_path = '/' . ltrim( $raw, '/' );
			return esc_url_raw( rtrim( $home, '/' ) . $raw_path );
		}

		return '';
	}

	/**
	 * Extract a supported pagination page number from a query string.
	 *
	 * Only `paged` and `page` are treated as static-cache variants.
	 * Any other query vars are ignored here to preserve current behavior.
	 *
	 * @param string $query_string
	 * @return int
	 */
	private function get_supported_pagination_page_number_from_query( $query_string ) {
		$query_string = (string) $query_string;
		if ( '' === $query_string ) {
			return 0;
		}

		$query_args = array();
		parse_str( ltrim( $query_string, '?' ), $query_args );
		if ( empty( $query_args ) || ! is_array( $query_args ) ) {
			return 0;
		}

		$paged_value = 0;
		foreach ( $query_args as $key => $value ) {
			$key = sanitize_key( $key );
			if ( 'paged' !== $key && 'page' !== $key ) {
				continue;
			}
			$paged_value = max( $paged_value, (int) $value );
		}

		return max( 0, $paged_value );
	}

	/**
	 * Get the active WordPress pagination base slug.
	 *
	 * @return string
	 */
	private function get_pagination_base_slug() {
		global $wp_rewrite;

		$pagination_base = 'page';
		if ( is_object( $wp_rewrite ) && ! empty( $wp_rewrite->pagination_base ) ) {
			$pagination_base = (string) $wp_rewrite->pagination_base;
		}

		$pagination_base = trim( sanitize_title( $pagination_base ), '/' );
		return ! empty( $pagination_base ) ? $pagination_base : 'page';
	}

	/**
	 * Get a regex pattern for supported pagination bases.
	 *
	 * @return string
	 */
	private function get_pagination_base_pattern() {
		$candidates = array( $this->get_pagination_base_slug(), 'page', 'pages' );
		$candidates = array_values( array_unique( array_filter( array_map( 'sanitize_title', $candidates ) ) ) );

		$escaped = array();
		foreach ( $candidates as $candidate ) {
			$escaped[] = preg_quote( $candidate, '#' );
		}

		return '(?:' . implode( '|', $escaped ) . ')';
	}

	/**
	 * Build a canonical paginated URL from a base listing/archive URL.
	 *
	 * @param string $base_url
	 * @param int    $page_number
	 * @return string
	 */
	private function build_paginated_url_from_base_url( $base_url, $page_number ) {
		$base_url    = esc_url_raw( $base_url );
		$page_number = (int) $page_number;

		if ( empty( $base_url ) ) {
			return '';
		}

		if ( $page_number < 2 ) {
			return $base_url;
		}

		return trailingslashit( $base_url ) . user_trailingslashit( $this->get_pagination_base_slug() . '/' . $page_number, 'paged' );
	}

	/**
	 * Get listing page URLs mapped for a given CPT.
	 *
	 * @param string $post_type
	 * @return array<int,string>
	 */
	public function get_cpt_listing_page_urls( $post_type ) {
		$post_type = sanitize_key( $post_type );
		$excluded_types = (array) get_option( 'wpsh_excluded_post_types', array() );
		if ( in_array( $post_type, $excluded_types, true ) ) {
			return array();
		}

		$map       = $this->get_cpt_listing_page_map();
		if ( empty( $map[ $post_type ] ) ) {
			return array();
		}

		$raw = $map[ $post_type ];

		// Allow multiline config (admin can paste multiple URLs, one per line).
		$parts = preg_split( "/\r\n|\n|\r/", (string) $raw );
		$urls  = array();
		foreach ( (array) $parts as $part ) {
			$url = $this->normalize_listing_mapping_entry_to_url( $part );
			if ( ! empty( $url ) ) {
				$urls[] = $url;
			}
		}

		$urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $urls ) ) ) );
		return $urls;
	}

	/**
	 * Get all mapped listing URLs for all CPTs.
	 *
	 * @return array<int,string>
	 */
	public function get_all_cpt_listing_page_urls() {
		$map  = $this->get_cpt_listing_page_map();
		$urls = array();
		$excluded_types = (array) get_option( 'wpsh_excluded_post_types', array() );

		foreach ( array_keys( $map ) as $post_type ) {
			if ( in_array( $post_type, $excluded_types, true ) ) {
				continue;
			}
			$urls = array_merge( $urls, $this->get_cpt_listing_page_urls( $post_type ) );
		}

		$urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $urls ) ) ) );
		return $urls;
	}

	/**
	 * Discover paginated listing URLs from a base listing page.
	 *
	 * This is a best-effort, bounded scan used only for explicitly mapped
	 * listing pages so existing archive generation performance remains stable.
	 *
	 * @param string $base_url
	 * @return array<int,string>
	 */
	public function discover_pagination_urls_from_base_url( $base_url ) {
		$base_url = esc_url_raw( $base_url );
		if ( empty( $base_url ) ) {
			return array();
		}

		static $discovery_cache = array();
		$cache_key = md5( untrailingslashit( $base_url ) );
		if ( isset( $discovery_cache[ $cache_key ] ) ) {
			return $discovery_cache[ $cache_key ];
		}

		$urls           = array();
		$listing_html   = $this->fetch_live_html_by_url( $base_url );
		$max_page_limit = max( 2, (int) apply_filters( 'wpsh_cpt_listing_pagination_scan_limit', 50, $base_url ) );

		if ( empty( $listing_html ) ) {
			$discovery_cache[ $cache_key ] = $urls;
			return $urls;
		}

		$base_path = wp_parse_url( $base_url, PHP_URL_PATH );
		$base_path = '/' . trim( (string) $base_path, '/' );
		if ( '/' !== $base_path ) {
			$base_path .= '/';
		}
		$normalized_base_path = untrailingslashit( '/' . trim( (string) wp_parse_url( $base_url, PHP_URL_PATH ), '/' ) );

		$pagination_path_pattern = '#^' . preg_quote( $base_path, '#' ) . $this->get_pagination_base_pattern() . '/([0-9]+)/?$#i';

		if ( preg_match_all( '/href\s*=\s*(["\'])(.*?)\1/i', $listing_html, $matches ) ) {
			foreach ( $matches[2] as $raw_url ) {
				$raw_url = html_entity_decode( trim( $raw_url ) );
				if ( empty( $raw_url ) || 0 === strpos( $raw_url, '#' ) ) {
					continue;
				}

				if ( 0 === strpos( $raw_url, '/' ) ) {
					$raw_url = home_url( $raw_url );
				}

				$candidate_url = esc_url_raw( $raw_url );
				if ( empty( $candidate_url ) ) {
					continue;
				}

				$candidate_path = wp_parse_url( $candidate_url, PHP_URL_PATH );
				if ( empty( $candidate_path ) ) {
					continue;
				}

				if ( preg_match( $pagination_path_pattern, $candidate_path, $page_matches ) ) {
					$page_number = (int) $page_matches[1];
					if ( $page_number >= 2 && $page_number <= $max_page_limit ) {
						$urls[ $page_number ] = $candidate_url;
					}
					continue;
				}

				$page_number = $this->get_supported_pagination_page_number_from_query( (string) wp_parse_url( $candidate_url, PHP_URL_QUERY ) );
				$normalized_candidate_path = untrailingslashit( '/' . trim( (string) $candidate_path, '/' ) );

				if ( $page_number >= 2 && $page_number <= $max_page_limit && $normalized_candidate_path === $normalized_base_path ) {
					$urls[ $page_number ] = $this->build_paginated_url_from_base_url( $base_url, $page_number );
				}
			}
		}

		// Fill in all pages between the minimum and maximum page numbers discovered.
		// This ensures that even if pagination UI only shows numbered links for nearby pages
		// and a "Last" link, we still cache all pages in between.
		if ( ! empty( $urls ) ) {
			$page_numbers = array_keys( $urls );
			$min_page = min( $page_numbers );
			$max_page = max( $page_numbers );

			// Generate URLs for all pages between min and max (inclusive).
			for ( $page_num = $min_page; $page_num <= $max_page; $page_num++ ) {
				if ( ! isset( $urls[ $page_num ] ) ) {
					$urls[ $page_num ] = $this->build_paginated_url_from_base_url( $base_url, $page_num );
				}
			}
		}

		ksort( $urls, SORT_NUMERIC );
		$discovery_cache[ $cache_key ] = $urls;
		return $urls;
	}

	/**
	 * Get paginated listing page URLs for a mapped CPT listing page.
	 *
	 * @param string $post_type
	 * @return array<int,string>
	 */
	public function get_cpt_listing_page_pagination_urls( $post_type ) {
		if ( ! (bool) apply_filters( 'wpsh_enable_cpt_listing_pagination_generation', true, $post_type ) ) {
			return array();
		}

		$urls = array();
		foreach ( $this->get_cpt_listing_page_urls( $post_type ) as $listing_url ) {
			$urls = array_merge( $urls, array_values( $this->discover_pagination_urls_from_base_url( $listing_url ) ) );
		}

		$urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $urls ) ) ) );
		return $urls;
	}

	/**
	 * Get all mapped listing URLs for a CPT that should be generated,
	 * including discovered paginated variants.
	 *
	 * @param string $post_type
	 * @return array<int,string>
	 */
	public function get_cpt_listing_generation_urls( $post_type ) {
		$urls = array_merge(
			$this->get_cpt_listing_page_urls( $post_type ),
			$this->get_cpt_listing_page_pagination_urls( $post_type )
		);

		$urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $urls ) ) ) );
		return $urls;
	}

	/**
	 * Get all mapped listing URLs across CPTs that should be generated,
	 * including discovered paginated variants.
	 *
	 * @return array<int,string>
	 */
	public function get_all_cpt_listing_generation_urls() {
		$map  = $this->get_cpt_listing_page_map();
		$urls = array();
		$excluded_types = (array) get_option( 'wpsh_excluded_post_types', array() );

		foreach ( array_keys( $map ) as $post_type ) {
			if ( in_array( $post_type, $excluded_types, true ) ) {
				continue;
			}
			$urls = array_merge( $urls, $this->get_cpt_listing_generation_urls( $post_type ) );
		}

		$urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $urls ) ) ) );
		return $urls;
	}

	/**
	 * Queue regeneration for mapped listing pages of a CPT.
	 *
	 * @param string|null $post_type
	 * @return void
	 */
	public function schedule_cpt_listing_page_regeneration( $post_type = null ) {
		if ( empty( $post_type ) ) return;

		$excluded_types = (array) get_option( 'wpsh_excluded_post_types', array() );
		if ( in_array( sanitize_key( $post_type ), $excluded_types, true ) ) {
			return;
		}

		$urls = $this->get_cpt_listing_generation_urls( $post_type );
		if ( empty( $urls ) ) return;
		$this->queue_generation_urls( $urls );
	}

	/**
	 * Whether auto-generation for post/term updates is enabled.
	 *
	 * @return bool
	 */
	private function is_auto_generation_on_save_enabled() {
		return (bool) apply_filters( 'wpsh_auto_generation_on_save_enabled', false );
	}

	/**
	 * Initialize the core hooks.
	 */
	public function init() {
		// Early serving check.
		add_action( 'plugins_loaded', array( $this, 'early_serve' ), 0 );
		
		// Content update hooks.
		if ( $this->is_auto_generation_on_save_enabled() ) {
			add_action( 'wp_after_insert_post', array( $this, 'regenerate_on_save' ), 20, 4 );
			add_action( 'save_post', array( $this, 'save_post_fallback' ), 25 );
			add_action( 'saved_term', array( $this, 'regenerate_on_term_save' ), 10, 3 );
		}
		add_action( 'wp_trash_post', array( $this, 'delete_static_on_remove' ) );
		add_action( 'before_delete_post', array( $this, 'delete_static_on_remove' ) );
		add_action( 'wpsh_process_generation_queue', array( $this, 'process_generation_queue' ) );
		
		// Admin cookie tracking.
		add_action( 'init', array( $this, 'set_admin_bypass_cookie' ) );
		add_action( 'clear_auth_cookie', array( $this, 'clear_admin_bypass_cookie' ) );
		if ( $this->is_woocommerce_active() ) {
			add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_store_api_nonce_override_script' ), 99 );
		}

		// Cached shop pages can serve stale Store API nonces for AJAX add-to-cart requests.
		if ( $this->is_woocommerce_active() ) {
			add_filter( 'woocommerce_store_api_disable_nonce_check', array( $this, 'maybe_disable_store_api_nonce_check_for_add_to_cart' ) );
		}
	}

	/**
	 * Set a bypass cookie for admins/editors.
	 */
	public function set_admin_bypass_cookie() {
		if ( is_user_logged_in() && current_user_can( 'edit_posts' ) ) {
			if ( ! isset( $_COOKIE['wpsh_admin_bypass'] ) ) {
				setcookie( 'wpsh_admin_bypass', '1', time() + 14 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
			}
		} else {
			if ( isset( $_COOKIE['wpsh_admin_bypass'] ) ) {
				setcookie( 'wpsh_admin_bypass', '0', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
			}
		}
	}

	/**
	 * Clear the bypass cookie on logout.
	 */
	public function clear_admin_bypass_cookie() {
		setcookie( 'wpsh_admin_bypass', '0', time() - 3600, COOKIEPATH, COOKIE_DOMAIN, is_ssl() );
	}

	/**
	 * Inject a frontend script that strips stale Store API nonce headers on shop listings.
	 *
	 * @return void
	 */
	public function enqueue_store_api_nonce_override_script() {
		if ( is_admin() || ! $this->should_enqueue_store_api_nonce_override() ) {
			return;
		}

		wp_register_script( 'wpsh-store-api-nonce-override', false, array(), WPSH_VERSION, true );
		wp_enqueue_script( 'wpsh-store-api-nonce-override' );
		wp_add_inline_script(
			'wpsh-store-api-nonce-override',
			"(function(){\n" .
			"var targets=['/wc/store/v1/batch','/wc/store/v1/cart/items','/wc/store/v1/cart/add-item','/wc/store/v1/cart/update-item'];\n" .
			"function normalize(value){\n" .
			"  if(!value){return '';}\n" .
			"  try{return new URL(String(value),window.location.origin).pathname.toLowerCase();}catch(e){return String(value).toLowerCase();}\n" .
			"}\n" .
			"function matches(value){\n" .
			"  var path=normalize(value);\n" .
			"  return targets.some(function(target){return path===target||path.slice(-target.length)===target;});\n" .
			"}\n" .
			"function stripHeaders(headers){\n" .
			"  if(!headers){return headers;}\n" .
			"  if(window.Headers&&headers instanceof Headers){headers.delete('Nonce');headers.delete('nonce');return headers;}\n" .
			"  if(Array.isArray(headers)){return headers.filter(function(entry){return !entry||String(entry[0]).toLowerCase()!=='nonce';});}\n" .
			"  if(typeof headers==='object'){Object.keys(headers).forEach(function(key){if(String(key).toLowerCase()==='nonce'){delete headers[key];}});}\n" .
			"  return headers;\n" .
			"}\n" .
			"function stripBatchPayload(payload){\n" .
			"  var data=payload;\n" .
			"  var isString=typeof payload==='string';\n" .
			"  if(isString){try{data=JSON.parse(payload);}catch(e){return payload;}}\n" .
			"  if(!data||!Array.isArray(data.requests)){return payload;}\n" .
			"  data.requests.forEach(function(request){if(request&&matches(request.path)&&request.headers){stripHeaders(request.headers);}});\n" .
			"  return isString?JSON.stringify(data):data;\n" .
			"}\n" .
			"if(typeof window.fetch==='function'){\n" .
			"  var originalFetch=window.fetch;\n" .
			"  window.fetch=function(input,init){\n" .
			"    var url=typeof input==='string'?input:(input&&input.url?input.url:'');\n" .
			"    var path=normalize(url);\n" .
			"    if(matches(path)){\n" .
			"      init=init||{};\n" .
			"      init.headers=stripHeaders(init.headers||(input&&input.headers?input.headers:undefined));\n" .
			"      if(path.slice(-'/wc/store/v1/batch'.length)==='/wc/store/v1/batch'&&init.body){init.body=stripBatchPayload(init.body);}\n" .
			"      if(window.Request&&input instanceof Request){input=new Request(input,init);init=undefined;}\n" .
			"    }\n" .
			"    return originalFetch.call(this,input,init);\n" .
			"  };\n" .
			"}\n" .
			"if(window.wp&&window.wp.apiFetch&&typeof window.wp.apiFetch.use==='function'){\n" .
			"  window.wp.apiFetch.use(function(options,next){\n" .
			"    var route=options&&(options.path||options.url)?(options.path||options.url):'';\n" .
			"    if(matches(route)){\n" .
			"      if(options.headers){stripHeaders(options.headers);}\n" .
			"      if(matches(route)&&options.data&&Array.isArray(options.data.requests)){options.data.requests.forEach(function(request){if(request&&matches(request.path)&&request.headers){stripHeaders(request.headers);}});}\n" .
			"    }\n" .
			"    return next(options);\n" .
			"  });\n" .
			"}\n" .
			"})();"
		);
	}

	/**
	 * Disable WooCommerce Store API nonce checks for add-to-cart requests.
	 *
	 * Cached shop pages can serve stale Store API nonces, so bypass validation
	 * only for the add-to-cart endpoints used by the shop listing.
	 *
	 * @param bool $disable_nonce_check Existing nonce bypass flag.
	 * @return bool
	 */
	public function maybe_disable_store_api_nonce_check_for_add_to_cart( $disable_nonce_check ) {
		if ( $disable_nonce_check ) {
			return (bool) $disable_nonce_check;
		}

		$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( 'POST' !== $request_method ) {
			return false;
		}

		if ( ! $this->is_store_api_add_to_cart_request() ) {
			return false;
		}

		return true;
	}

	/**
	 * Check whether the current request targets a Store API add-to-cart endpoint.
	 *
	 * @return bool
	 */
	private function is_store_api_add_to_cart_request() {
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$parsed_uri  = wp_parse_url( $request_uri );
		$request_path = isset( $parsed_uri['path'] ) ? (string) $parsed_uri['path'] : '';
		$rest_route   = '';

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( isset( $_GET['rest_route'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
			$rest_route = sanitize_text_field( wp_unslash( $_GET['rest_route'] ) );
		} elseif ( ! empty( $parsed_uri['query'] ) ) {
			$query_args = array();
			parse_str( (string) $parsed_uri['query'], $query_args );
			if ( isset( $query_args['rest_route'] ) ) {
				$rest_route = (string) $query_args['rest_route'];
			}
		}

		$candidate_routes = array_filter(
			array(
				strtolower( '/' . ltrim( $request_path, '/' ) ),
				strtolower( '/' . ltrim( $rest_route, '/' ) ),
			)
		);

		$store_api_endpoints = array(
			'/wc/store/v1/batch',
			'/wc/store/v1/cart/items',
			'/wc/store/v1/cart/add-item',
			'/wc/store/v1/cart/update-item',
		);

		foreach ( $candidate_routes as $candidate_route ) {
			foreach ( $store_api_endpoints as $store_api_endpoint ) {
				if ( '/wc/store/v1/batch' === $store_api_endpoint && ( $candidate_route === $store_api_endpoint || ( strlen( $candidate_route ) > strlen( $store_api_endpoint ) && substr( $candidate_route, -strlen( $store_api_endpoint ) ) === $store_api_endpoint ) ) ) {
					return $this->batch_request_contains_store_api_add_to_cart();
				}

				if ( $candidate_route === $store_api_endpoint ) {
					return true;
				}

				if ( strlen( $candidate_route ) > strlen( $store_api_endpoint ) && substr( $candidate_route, -strlen( $store_api_endpoint ) ) === $store_api_endpoint ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Check whether the current Store API batch request includes add-to-cart operations.
	 *
	 * @return bool
	 */
	private function batch_request_contains_store_api_add_to_cart() {
		$raw_body = file_get_contents( 'php://input' );
		if ( empty( $raw_body ) ) {
			return false;
		}

		$payload = map_deep( json_decode( $raw_body, true ), 'sanitize_text_field' );
		if ( empty( $payload['requests'] ) || ! is_array( $payload['requests'] ) ) {
			return false;
		}

		$target_paths = array(
			'/wc/store/v1/cart/items',
			'/wc/store/v1/cart/add-item',
			'/wc/store/v1/cart/update-item',
		);

		foreach ( $payload['requests'] as $request ) {
			if ( empty( $request['path'] ) ) {
				continue;
			}

			$path = strtolower( (string) wp_parse_url( (string) $request['path'], PHP_URL_PATH ) );
			foreach ( $target_paths as $target_path ) {
				if ( $path === $target_path || ( strlen( $path ) > strlen( $target_path ) && substr( $path, -strlen( $target_path ) ) === $target_path ) ) {
					return true;
				}
			}
		}

		return false;
	}

	/**
	 * Determine whether the current request is a WooCommerce product listing page.
	 *
	 * @return bool
	 */
	private function should_enqueue_store_api_nonce_override() {
		if ( ! $this->is_woocommerce_active() ) {
			return false;
		}
		if ( ! function_exists( 'is_shop' ) ) {
			return false;
		}

		return is_shop() || is_product_taxonomy() || is_product_category() || is_product_tag() || is_post_type_archive( 'product' );
	}

	/**
	 * Early serve static HTML if it exists.
	 */
	public function early_serve() {
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) return;

		// Only serve static files for GET requests. POST/PUT/DELETE/PATCH requests
		// (e.g. WooCommerce add-to-cart form submissions, checkout) must be processed
		// by WordPress, not intercepted and served as cached HTML.
		$request_method = isset( $_SERVER['REQUEST_METHOD'] ) ? strtoupper( sanitize_text_field( wp_unslash( $_SERVER['REQUEST_METHOD'] ) ) ) : 'GET';
		if ( 'GET' !== $request_method && 'HEAD' !== $request_method ) return;

		$skip_header = isset( $_SERVER['HTTP_X_WPSH_SKIP'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_WPSH_SKIP'] ) ) : '';
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$skip_param  = isset( $_GET['wpsh_skip'] ) ? sanitize_text_field( wp_unslash( $_GET['wpsh_skip'] ) ) : '';
		if ( '1' === $skip_header || '1' === $skip_param ) return;

		// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$request_uri_raw = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$request_uri     = $request_uri_raw;
		$lookup_uri      = $request_uri;

		// Dynamic requests should bypass static serving across different site setups.
		$dynamic_query_args_default = array();
		if ( $this->is_woocommerce_active() ) {
			$dynamic_query_args_default = array(
				'wc-ajax',
				'add-to-cart',
				'added-to-cart',
				'remove_item',
				'undo_item',
				'update_cart',
				'apply_coupon',
				'remove_coupon',
			);
		}
		$dynamic_query_args = (array) apply_filters(
			'wpsh_dynamic_query_args',
			$dynamic_query_args_default
		);
		$dynamic_query_args = array_filter( array_map( 'sanitize_key', $dynamic_query_args ) );
		foreach ( $dynamic_query_args as $dynamic_arg ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended
			if ( isset( $_GET[ $dynamic_arg ] ) ) {
				return;
			}
		}

		$parsed_request_uri = wp_parse_url( $request_uri_raw );
		$request_path       = isset( $parsed_request_uri['path'] ) ? (string) $parsed_request_uri['path'] : '/';
		$home_path          = (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH );
		$request_path_rel   = trim( (string) preg_replace( '#^' . preg_quote( untrailingslashit( $home_path ), '#' ) . '#', '', $request_path ), '/' );

		$dynamic_path_prefixes_default = array();
		if ( $this->is_woocommerce_active() ) {
			$dynamic_path_prefixes_default = array(
				'cart',
				'checkout',
				'my-account',
			);
		}
		$dynamic_path_prefixes = (array) apply_filters(
			'wpsh_dynamic_path_prefixes',
			$dynamic_path_prefixes_default
		);
		$dynamic_path_prefixes = array_values( array_filter( array_map( 'sanitize_title', $dynamic_path_prefixes ) ) );
		foreach ( $dynamic_path_prefixes as $path_prefix ) {
			if ( $request_path_rel === $path_prefix || 0 === strpos( $request_path_rel, $path_prefix . '/' ) ) {
				return;
			}
		}

		$dynamic_cookie_names = (array) apply_filters(
			'wpsh_dynamic_cookie_names',
			array()
		);
		$dynamic_cookie_names = array_filter( array_map( 'sanitize_key', $dynamic_cookie_names ) );
		foreach ( $dynamic_cookie_names as $cookie_name ) {
			if ( isset( $_COOKIE[ $cookie_name ] ) ) {
				return;
			}
		}

		$dynamic_cookie_prefixes = (array) apply_filters(
			'wpsh_dynamic_cookie_prefixes',
			array()
		);
		$dynamic_cookie_prefixes = array_values( array_filter( array_map( 'sanitize_key', $dynamic_cookie_prefixes ) ) );
		foreach ( array_keys( $_COOKIE ) as $cookie_name ) {
			$cookie_name = sanitize_key( (string) $cookie_name );
			foreach ( $dynamic_cookie_prefixes as $cookie_prefix ) {
				if ( 0 === strpos( $cookie_name, $cookie_prefix ) ) {
					return;
				}
			}
		}

		if ( isset( $_COOKIE['wordpress_logged_in_' . COOKIEHASH] ) ) {
			if ( isset( $_COOKIE['wpsh_admin_bypass'] ) && '1' === $_COOKIE['wpsh_admin_bypass'] ) {
				return;
			}
			$allow_logged_in_cache = false;
			$allowed_paths = array();
			
			if ( $this->is_woocommerce_active() ) {
				if ( function_exists( 'wc_get_page_id' ) ) {
					$shop_page_id = wc_get_page_id( 'shop' );
					if ( $shop_page_id ) {
						$shop_page = get_post( $shop_page_id );
						if ( $shop_page ) {
							$allowed_paths[] = '^' . preg_quote( $shop_page->post_name, '#' ) . '(/|$)';
						}
					}
					$product_permalink = get_option( 'woocommerce_permalinks' );
					$product_base = ! empty( $product_permalink['product_base'] ) ? trim( $product_permalink['product_base'], '/' ) : 'product';
					$product_base_parts = explode( '/', $product_base );
					$first_part = $product_base_parts[0];
					if ( strpos( $first_part, '%' ) === false ) {
						$allowed_paths[] = '^' . preg_quote( $first_part, '#' ) . '(/|$)';
					} else {
						$allowed_paths[] = '^' . preg_quote( 'product', '#' ) . '(/|$)';
					}
				}
			}
			
			$allowed_paths = (array) apply_filters( 'wpsh_logged_in_cache_paths_rel', $allowed_paths );
			
			foreach ( $allowed_paths as $pattern ) {
				if ( preg_match( '#' . $pattern . '#i', $request_path_rel ) ) {
					$allow_logged_in_cache = true;
					break;
				}
			}

			if ( ! $allow_logged_in_cache ) {
				return;
			}
		}

		// Support numeric query pagination (e.g. ?paged=2 / ?page=2) by
		// mapping it to the generated pretty-permalink static path (/page/2/).
		$parsed_request_uri = wp_parse_url( $request_uri_raw );
		$query_string       = isset( $parsed_request_uri['query'] ) ? (string) $parsed_request_uri['query'] : '';
		if ( '' !== $query_string ) {
			$query_args = array();
			parse_str( $query_string, $query_args );

			$query_keys   = array_map( 'sanitize_key', array_keys( $query_args ) );
			$allowed_keys = (array) apply_filters(
				'wpsh_cache_safe_query_args',
				array(
					'paged',
					'page',
					'utm_source',
					'utm_medium',
					'utm_campaign',
					'utm_term',
					'utm_content',
					'gclid',
					'fbclid',
					'_ga',
					'_gl',
					'mc_cid',
					'mc_eid',
					'srsltid',
				)
			);
			$allowed_keys   = array_map( 'sanitize_key', $allowed_keys );
			$unsupported_qs = array_diff( $query_keys, $allowed_keys );
			if ( ! empty( $unsupported_qs ) ) {
				return;
			}

			$paged_value = 0;
			if ( isset( $query_args['paged'] ) ) {
				$paged_value = max( $paged_value, (int) $query_args['paged'] );
			}
			if ( isset( $query_args['page'] ) ) {
				$paged_value = max( $paged_value, (int) $query_args['page'] );
			}

			if ( $paged_value > 1 ) {
				$base_path = isset( $parsed_request_uri['path'] ) ? (string) $parsed_request_uri['path'] : '/';
				$lookup_uri = trailingslashit( $base_path ) . user_trailingslashit( $this->get_pagination_base_slug() . '/' . $paged_value, 'paged' );
			} else {
				$lookup_uri = isset( $parsed_request_uri['path'] ) ? (string) $parsed_request_uri['path'] : '/';
			}
		}

		// Get current relative path for redirection check.
		$current_path = WPSH_Helper::clean_redir_source( $request_uri );
		$redirs = get_option( 'wpsh_redirections', array() );
		
		if ( ! empty( $redirs ) ) {
			foreach ( $redirs as $idx => $wpsh_r ) {
				if ( empty( $wpsh_r['active'] ) || $wpsh_r['active'] === '0' ) continue;
				if ( WPSH_Helper::clean_redir_source( $wpsh_r['source'] ) === $current_path ) {
					// Update hits.
					$redirs[$idx]['hits'] = ( isset( $redirs[$idx]['hits'] ) ? $redirs[$idx]['hits'] : 0 ) + 1;
					update_option( 'wpsh_redirections', $redirs );
					
					$code = intval( $wpsh_r['code'] ) ?: 301;
					wp_safe_redirect( $wpsh_r['target'], $code );
					exit;
				}
			}
		}

		$http_host = isset( $_SERVER['HTTP_HOST'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
		$url  = ( isset( $_SERVER['HTTPS'] ) && $_SERVER['HTTPS'] === 'on' ? "https" : "http" ) . "://" . $http_host . $lookup_uri;
		$path = $this->get_static_path_from_url( $url );

		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		if ( $path && $wp_filesystem->exists( $path ) ) {
			header( 'Content-Type: text/html; charset=UTF-8' );
			header( 'X-WPSH-Served: Static (Early PHP)' );
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $wp_filesystem->get_contents( $path );
			exit;
		}
	}

	/**
	 * Get absolute file path from URL.
	 */
	public function get_static_path_from_url( $url ) {
		if ( empty( $url ) ) return false;
		
		$path    = wp_parse_url( $url, PHP_URL_PATH );
		$query   = wp_parse_url( $url, PHP_URL_QUERY );
		$wp_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );

		if ( ! empty( $query ) ) {
			$query_args = array();
			parse_str( (string) $query, $query_args );

			$query_keys   = array_map( 'sanitize_key', array_keys( $query_args ) );
			$allowed_keys = (array) apply_filters(
				'wpsh_cache_safe_query_args',
				array(
					'paged',
					'page',
					'utm_source',
					'utm_medium',
					'utm_campaign',
					'utm_term',
					'utm_content',
					'gclid',
					'fbclid',
					'_ga',
					'_gl',
					'mc_cid',
					'mc_eid',
					'srsltid',
				)
			);
			$allowed_keys   = array_map( 'sanitize_key', $allowed_keys );
			$unsupported_qs = array_diff( $query_keys, $allowed_keys );
			if ( ! empty( $unsupported_qs ) ) {
				return false;
			}
		}
		
		// Normalize paths for comparison (ensure both have leading/trailing slashes)
		$path_norm = '/' . trim( (string) $path, '/' ) . '/';
		$wp_path_norm = '/' . trim( (string) $wp_path, '/' ) . '/';
		
		// If the path starts with the WP base path, strip it.
		if ( $wp_path_norm !== '/' && 0 === strpos( $path_norm, $wp_path_norm ) ) {
			$path_final = substr( $path_norm, strlen( $wp_path_norm ) );
		} else {
			$path_final = $path; // Fallback to original path if no match
		}
		
		$path_final = trim( (string) $path_final, '/' );
		$paged_value = $this->get_supported_pagination_page_number_from_query( (string) $query );
		if ( $paged_value > 1 ) {
			$path_final = trim( $path_final . '/' . $this->get_pagination_base_slug() . '/' . $paged_value, '/' );
		}
		
		if ( empty( $path_final ) ) {
			$file_path = WPSH_Helper::get_dir() . '/index.html';
		} else {
			$file_path = WPSH_Helper::get_dir() . '/' . $path_final . '/index.html';
		}
		return wp_normalize_path( $file_path );
	}

	/**
	 * Get public post types eligible for archive generation.
	 *
	 * @return array<string, WP_Post_Type>
	 */
	public function get_public_archive_post_types() {
		$post_types     = get_post_types( array( 'public' => true ), 'objects' );
		$excluded_types = (array) get_option( 'wpsh_excluded_post_types', array() );
		$eligible_types = array();

		foreach ( $post_types as $post_type => $post_type_object ) {
			if ( in_array( $post_type, $excluded_types, true ) ) {
				continue;
			}
			if ( 'attachment' === $post_type ) {
				continue;
			}
			if ( 'post' === $post_type || ! empty( $post_type_object->has_archive ) ) {
				$eligible_types[ $post_type ] = $post_type_object;
			}
		}

		return $eligible_types;
	}

	/**
	 * Check whether a post type has an archive we can regenerate.
	 *
	 * @param string $post_type Post type slug.
	 * @return bool
	 */
	public function post_type_has_archive_support( $post_type ) {
		$post_type  = sanitize_key( $post_type );
		$post_types = $this->get_public_archive_post_types();

		return isset( $post_types[ $post_type ] );
	}

	/**
	 * Get the archive base URL for a post type.
	 *
	 * @param string $post_type Post type slug.
	 * @return string
	 */
	public function get_post_type_archive_base_url( $post_type ) {
		$post_type = sanitize_key( $post_type );

		if ( 'post' === $post_type ) {
			$posts_page_id = (int) get_option( 'page_for_posts' );
			if ( $posts_page_id ) {
				$posts_page_url = get_permalink( $posts_page_id );
				if ( $posts_page_url ) {
					return $posts_page_url;
				}
			}

			// If a custom blog/listing page is mapped for regular posts, prefer that
			// before falling back to the site home page. This keeps the special handling
			// limited to blog pages only.
			$mapped_post_listing_urls = $this->get_cpt_listing_page_urls( 'post' );
			if ( ! empty( $mapped_post_listing_urls ) ) {
				return (string) reset( $mapped_post_listing_urls );
			}

			return home_url( '/' );
		}

		if ( ! $this->post_type_has_archive_support( $post_type ) ) {
			return '';
		}

		$archive_link = get_post_type_archive_link( $post_type );
		if ( $archive_link && ! is_wp_error( $archive_link ) ) {
			return $archive_link;
		}

		$post_type_object = get_post_type_object( $post_type );
		if ( $post_type_object && is_string( $post_type_object->has_archive ) && ! empty( $post_type_object->has_archive ) ) {
			return home_url( user_trailingslashit( $post_type_object->has_archive ) );
		}

		return '';
	}

	/**
	 * Get the posts-per-page value used for a post type archive.
	 *
	 * @param string $post_type Post type slug.
	 * @return int
	 */
	public function get_post_type_archive_posts_per_page( $post_type ) {
		$post_type      = sanitize_key( $post_type );
		$posts_per_page = (int) get_option( 'posts_per_page', 10 );

		if ( 'product' === $post_type && function_exists( 'wc_get_default_products_per_row' ) && function_exists( 'wc_get_default_product_rows_per_page' ) ) {
			$default_products_per_page = (int) wc_get_default_products_per_row() * (int) wc_get_default_product_rows_per_page();
			if ( $default_products_per_page > 0 ) {
				// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound
				$posts_per_page = (int) apply_filters( 'loop_shop_per_page', $default_products_per_page );
			}
		}

		$posts_per_page = (int) apply_filters( 'wpsh_archive_posts_per_page', $posts_per_page, $post_type );
		return max( 1, $posts_per_page );
	}

	/**
	 * Get the total number of archive pages for a post type.
	 *
	 * @param string $post_type Post type slug.
	 * @return int
	 */
	public function get_post_type_archive_total_pages( $post_type ) {
		$post_type = sanitize_key( $post_type );
		if ( ! $this->post_type_has_archive_support( $post_type ) ) {
			return 1;
		}

		$posts_per_page = $this->get_post_type_archive_posts_per_page( $post_type );
		$post_counts    = wp_count_posts( $post_type );
		$total_posts    = ( $post_counts && isset( $post_counts->publish ) ) ? (int) $post_counts->publish : 0;

		if ( $total_posts < 1 ) {
			return 1;
		}

		return max( 1, (int) ceil( $total_posts / $posts_per_page ) );
	}

	/**
	 * Get the paginated archive URL for a post type and page number.
	 *
	 * @param string $post_type   Post type slug.
	 * @param int    $page_number Archive page number.
	 * @return string
	 */
	public function get_post_type_archive_pagination_url( $post_type, $page_number ) {
		$page_number = (int) $page_number;
		if ( $page_number < 2 ) {
			return $this->get_post_type_archive_base_url( $post_type );
		}

		$base_url = trailingslashit( $this->get_post_type_archive_base_url( $post_type ) );
		if ( empty( $base_url ) ) {
			return '';
		}

		return $base_url . user_trailingslashit( $this->get_pagination_base_slug() . '/' . $page_number, 'paged' );
	}

	/**
	 * Discover paginated archive URLs from the live archive markup.
	 *
	 * @param string $post_type Post type slug.
	 * @return array<int, string>
	 */
	public function discover_post_type_archive_pagination_urls( $post_type ) {
		$post_type = sanitize_key( $post_type );
		$base_url  = $this->get_post_type_archive_base_url( $post_type );
		$urls      = array();

		if ( empty( $base_url ) ) {
			return $urls;
		}

		$archive_html = $this->fetch_live_html_by_url( $base_url );
		if ( empty( $archive_html ) ) {
			return $urls;
		}

		$base_path = wp_parse_url( $base_url, PHP_URL_PATH );
		$base_path = '/' . trim( (string) $base_path, '/' );
		if ( '/' !== $base_path ) {
			$base_path .= '/';
		}

		if ( preg_match_all( '/href\s*=\s*(["\'])(.*?)\1/i', $archive_html, $matches ) ) {
			foreach ( $matches[2] as $raw_url ) {
				$raw_url = html_entity_decode( trim( $raw_url ) );
				if ( empty( $raw_url ) ) {
					continue;
				}

				if ( 0 === strpos( $raw_url, '/' ) ) {
					$raw_url = home_url( $raw_url );
				}

				$candidate_url = esc_url_raw( $raw_url );
				$candidate_path = wp_parse_url( $candidate_url, PHP_URL_PATH );
				if ( empty( $candidate_path ) ) {
					continue;
				}

				if ( preg_match( '#^' . preg_quote( $base_path, '#' ) . 'page/([0-9]+)/?$#i', $candidate_path, $page_matches ) ) {
					$page_number = (int) $page_matches[1];
					if ( $page_number >= 2 ) {
						$urls[ $page_number ] = $candidate_url;
					}
				}
			}
		}

		ksort( $urls, SORT_NUMERIC );
		return $urls;
	}

	/**
	 * Get paginated archive URLs for a post type.
	 *
	 * @param string $post_type Post type slug.
	 * @param int    $offset    Batch offset.
	 * @param int    $limit     Batch size.
	 * @return array<int, string>
	 */
	public function get_post_type_archive_pagination_urls( $post_type, $offset = 0, $limit = 0 ) {
		$offset = max( 0, (int) $offset );
		$limit  = (int) $limit;
		$urls   = array();

		if ( empty( get_option( 'permalink_structure' ) ) ) {
			WPSH_Logger::warning( 'Skipping archive pagination generation because pretty permalinks are disabled.', array( 'post_type' => $post_type ) );
			return $urls;
		}

		if ( ! $this->post_type_has_archive_support( $post_type ) ) {
			return $urls;
		}

		// Calculate total pages mathematically
		$max_pages = $this->get_post_type_archive_total_pages( $post_type );
		if ( $max_pages >= 2 ) {
			$page_numbers = range( 2, $max_pages );
			foreach ( $page_numbers as $page_number ) {
				$url = $this->get_post_type_archive_pagination_url( $post_type, $page_number );
				if ( ! empty( $url ) ) {
					$urls[ $page_number ] = $url;
				}
			}
		}

		// Live discovery performs extra loopback HTTP requests and can make bulk generation
		// unacceptably slow on shared or lower-powered hosting. Keep it opt-in for most
		// archives, but enable it by default for the blog archive so custom blog pagination
		// paths like `/blogs/pages/2/` are discovered correctly.
		$default_live_discovery = ( 'post' === sanitize_key( $post_type ) );
		$use_live_discovery     = (bool) apply_filters( 'wpsh_enable_live_pagination_discovery', $default_live_discovery, $post_type );
		
		if ( $use_live_discovery ) {
			$discovered_urls = $this->discover_post_type_archive_pagination_urls( $post_type );
			
			// Override baseline URLs with any matching keys from discovery, to support custom formats
			if ( ! empty( $discovered_urls ) ) {
				foreach ( $discovered_urls as $page_number => $url ) {
					$urls[ $page_number ] = $url;
				}
			}
		}

		ksort( $urls, SORT_NUMERIC );

		if ( $offset > 0 || $limit > 0 ) {
			$urls = array_slice( $urls, $offset, ( $limit > 0 ? $limit : null ), true );
		}

		return $urls;
	}

	/**
	 * Remove stale static files for a post type's paginated archive pages.
	 *
	 * @param string $post_type Post type slug.
	 */
	public function cleanup_stale_post_type_pagination( $post_type ) {
		$post_type = sanitize_key( $post_type );
		if ( ! $this->post_type_has_archive_support( $post_type ) ) {
			return;
		}
		// Automatic deletion of stale pagination files has been disabled
		// to prevent removal of generated static HTML without explicit action.
	}

	/**
	 * Remove stale archive pagination for all eligible post types.
	 */
	public function cleanup_all_stale_archive_pagination() {
		foreach ( array_keys( $this->get_public_archive_post_types() ) as $post_type ) {
			$this->cleanup_stale_post_type_pagination( $post_type );
		}
	}

	/**
	 * Regenerate paginated archive pages for a post type.
	 *
	 * @param string $post_type Post type slug.
	 * @param int    $offset    Batch offset.
	 * @param int    $limit     Batch size.
	 * @return array<int, bool|string>
	 */
	public function regenerate_post_type_pagination( $post_type, $offset = 0, $limit = 0 ) {
		$results = array();
		$urls    = $this->get_post_type_archive_pagination_urls( $post_type, $offset, $limit );

		foreach ( $urls as $page_number => $url ) {
			$results[ $page_number ] = $this->generate_html_by_url( $url );
		}

		if ( 0 === (int) $offset && 0 === (int) $limit ) {
			$this->cleanup_stale_post_type_pagination( $post_type );
		}

		return $results;
	}

	/**
	 * Get the total number of paginated archive pages across all eligible post types.
	 *
	 * @return int
	 */
	public function get_total_archive_pagination_pages() {
		$total = 0;

		foreach ( array_keys( $this->get_public_archive_post_types() ) as $post_type ) {
			$max_pages = (int) $this->get_post_type_archive_total_pages( $post_type );
			if ( $max_pages > 1 ) {
				$total += ( $max_pages - 1 );
			}
		}

		return max( 0, $total );
	}

	/**
	 * Get the base archive URLs that should be generated for public post types.
	 *
	 * @return array<int, string>
	 */
	public function get_archive_base_urls() {
		$urls = array( home_url( '/' ) );

		foreach ( array_keys( $this->get_public_archive_post_types() ) as $post_type ) {
			$archive_url = $this->get_post_type_archive_base_url( $post_type );
			if ( ! empty( $archive_url ) ) {
				$urls[] = $archive_url;
			}
		}

		$urls = array_filter( array_map( 'esc_url_raw', $urls ) );
		return array_values( array_unique( $urls ) );
	}

	/**
	 * Get paginated archive generation items for all eligible post types.
	 *
	 * @param int $offset Batch offset.
	 * @param int $limit  Batch size.
	 * @return array<int, array<string, mixed>>
	 */
	public function get_archive_pagination_items( $offset = 0, $limit = 0 ) {
		$offset  = max( 0, (int) $offset );
		$limit   = (int) $limit;
		$items   = array();
		$skipped = 0;

		foreach ( array_keys( $this->get_public_archive_post_types() ) as $post_type ) {
			foreach ( $this->get_post_type_archive_pagination_urls( $post_type ) as $page_number => $url ) {
				if ( $skipped < $offset ) {
					$skipped++;
					continue;
				}

				$items[] = array(
					'post_type'   => $post_type,
					'page_number' => (int) $page_number,
					'url'         => $url,
				);

				if ( $limit > 0 && count( $items ) >= $limit ) {
					return $items;
				}
			}
		}

		return $items;
	}

	/**
	 * Regenerate all relevant archive URLs for a post type.
	 *
	 * @param string $post_type Post type slug.
	 */
	public function regenerate_post_type_archives( $post_type ) {
		$post_type = sanitize_key( $post_type );
		if ( ! $this->post_type_has_archive_support( $post_type ) ) {
			return;
		}

		$home_url = home_url( '/' );
		$this->generate_html_by_url( $home_url );

		$archive_url = $this->get_post_type_archive_base_url( $post_type );
		if ( $archive_url && untrailingslashit( $archive_url ) !== untrailingslashit( $home_url ) ) {
			$this->generate_html_by_url( $archive_url );
		}

		$this->regenerate_post_type_pagination( $post_type );
	}

	/**
	 * Get all relevant archive URLs for a post type or for all eligible post types.
	 *
	 * @param string|null $post_type Optional post type slug.
	 * @return array<int, string>
	 */
	public function get_archive_urls( $post_type = null ) {
		$urls = array();

		if ( ! empty( $post_type ) ) {
			$post_type = sanitize_key( $post_type );
			if ( ! $this->post_type_has_archive_support( $post_type ) ) {
				return $urls;
			}

			$urls[] = home_url( '/' );
			$archive_url = $this->get_post_type_archive_base_url( $post_type );
			if ( ! empty( $archive_url ) ) {
				$urls[] = $archive_url;
			}
			$urls = array_merge( $urls, array_values( $this->get_post_type_archive_pagination_urls( $post_type ) ) );
		} else {
			$urls = $this->get_archive_base_urls();
			foreach ( array_keys( $this->get_public_archive_post_types() ) as $archive_post_type ) {
				$urls = array_merge( $urls, array_values( $this->get_post_type_archive_pagination_urls( $archive_post_type ) ) );
			}
		}

		$urls = array_filter( array_map( 'esc_url_raw', $urls ) );
		return array_values( array_unique( $urls ) );
	}

	/**
	 * Queue URLs for delayed background generation.
	 *
	 * @param array<int, string> $urls URLs to queue.
	 */
	public function queue_generation_urls( $urls ) {
		$queue = get_option( 'wpsh_generation_queue', array() );
		if ( ! is_array( $queue ) ) {
			$queue = array();
		}

		$normalized_queue = array();
		foreach ( $queue as $queued_url ) {
			$queued_url = esc_url_raw( $queued_url );
			if ( ! empty( $queued_url ) ) {
				$normalized_queue[ $queued_url ] = $queued_url;
			}
		}

		foreach ( (array) $urls as $queued_url ) {
			$queued_url = esc_url_raw( $queued_url );
			if ( ! empty( $queued_url ) ) {
				$normalized_queue[ $queued_url ] = $queued_url;
			}
		}

		update_option( 'wpsh_generation_queue', array_values( $normalized_queue ), false );

		if ( ! wp_next_scheduled( 'wpsh_process_generation_queue' ) ) {
			wp_schedule_single_event( time() + 15, 'wpsh_process_generation_queue' );
		}
	}

	/**
	 * Schedule a delayed archive regeneration pass.
	 *
	 * @param string|null $post_type Optional post type slug.
	 */
	public function schedule_archive_regeneration( $post_type = null ) {
		$urls = $this->get_archive_urls( $post_type );
		if ( ! empty( $urls ) ) {
			WPSH_Logger::info( 'Queueing background archive regeneration', array( 'post_type' => $post_type, 'urls' => $urls ) );
			$this->queue_generation_urls( $urls );
		}
	}

	/**
	 * Check whether a generation error looks temporary and safe to retry.
	 *
	 * @param mixed $result
	 * @return bool
	 */
	public function is_retryable_generation_error( $result ) {
		if ( true === $result || empty( $result ) || ! is_string( $result ) ) {
			return false;
		}

		$retryable_markers = array(
			'cURL error 28',
			'Operation timed out',
			'Resolving timed out',
			'Connection timed out',
			'OpenSSL SSL_read',
			'Empty Response Body',
			'HTTP Error 408',
			'HTTP Error 425',
			'HTTP Error 429',
			'HTTP Error 500',
			'HTTP Error 502',
			'HTTP Error 503',
			'HTTP Error 504',
		);

		foreach ( $retryable_markers as $marker ) {
			if ( false !== stripos( $result, $marker ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Queue a URL for background regeneration when the error is retryable.
	 *
	 * @param string      $url
	 * @param bool|string $result
	 * @return bool
	 */
	public function maybe_queue_retryable_generation( $url, $result ) {
		$url = esc_url_raw( $url );
		if ( empty( $url ) || ! $this->is_retryable_generation_error( $result ) ) {
			return false;
		}

		$this->queue_generation_urls( array( $url ) );
		WPSH_Logger::warning(
			'Static generation timed out; URL queued for background retry',
			array(
				'url'   => $url,
				'error' => $result,
			)
		);

		return true;
	}

	/**
	 * Fetch fresh live HTML for a URL while bypassing static serving.
	 *
	 * @param string $url Target URL.
	 * @return string
	 */
	public function fetch_live_html_by_url( $url ) {
		if ( empty( $url ) ) {
			return '';
		}

		$request_url = $url;
		$parsed      = wp_parse_url( $url );
		$host        = isset( $parsed['host'] ) ? $parsed['host'] : '';
		$timeout     = max( 5, (int) apply_filters( 'wpsh_pagination_discovery_timeout', 15, $url ) );
		$args        = array(
			'timeout'     => $timeout,
			'sslverify'   => false,
			'cookies'     => array(),
			'redirection' => 5,
			'headers'     => array(
				'Host'          => $host,
				'Cache-Control' => 'no-cache',
				'Pragma'        => 'no-cache',
				'X-WPSH-Skip'   => '1',
			),
		);

		$home_url_host = wp_parse_url( home_url(), PHP_URL_HOST );
		if ( $host === $home_url_host ) {
			$request_url = str_replace( $host, '127.0.0.1', $url );
		}

		$response = wp_remote_get( $request_url, $args );
		if ( is_wp_error( $response ) && $request_url !== $url ) {
			$response = wp_remote_get( $url, $args );
		}
		if ( is_wp_error( $response ) ) {
			return '';
		}

		$code = wp_remote_retrieve_response_code( $response );
		if ( 200 !== (int) $code && $request_url !== $url ) {
			$response = wp_remote_get( $url, $args );
			$code     = wp_remote_retrieve_response_code( $response );
		}
		if ( 200 !== (int) $code ) {
			return '';
		}

		$html = wp_remote_retrieve_body( $response );
		return is_string( $html ) ? $html : '';
	}

	/**
	 * Process the queued background generation URLs.
	 */
	public function process_generation_queue() {
		$queue = get_option( 'wpsh_generation_queue', array() );
		if ( empty( $queue ) || ! is_array( $queue ) ) {
			return;
		}

		$batch_size = max( 1, (int) apply_filters( 'wpsh_background_generation_batch_size', 5 ) );
		$batch      = array_slice( $queue, 0, $batch_size );
		$remaining  = array_slice( $queue, count( $batch ) );

		foreach ( $batch as $queued_url ) {
			$queued_url = esc_url_raw( $queued_url );
			if ( empty( $queued_url ) ) {
				continue;
			}

			$result = $this->generate_html_by_url( $queued_url );
			if ( true !== $result ) {
				WPSH_Logger::warning(
					'Background static regeneration failed; URL left in queue for retry',
					array(
						'url'   => $queued_url,
						'error' => $result,
					)
				);
				$remaining[] = $queued_url;
			}
		}

		$remaining = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $remaining ) ) ) );
		update_option( 'wpsh_generation_queue', $remaining, false );

		if ( ! empty( $remaining ) && ! wp_next_scheduled( 'wpsh_process_generation_queue' ) ) {
			wp_schedule_single_event( time() + 60, 'wpsh_process_generation_queue' );
		}
	}

	/**
	 * Backward-compatible wrapper for the blog archive base URL.
	 *
	 * @return string
	 */
	public function get_blog_archive_base_url() {
		return $this->get_post_type_archive_base_url( 'post' );
	}

	/**
	 * Backward-compatible wrapper for the blog archive page count.
	 *
	 * @return int
	 */
	public function get_blog_pagination_total_pages() {
		return $this->get_post_type_archive_total_pages( 'post' );
	}

	/**
	 * Backward-compatible wrapper for a blog paginated URL.
	 *
	 * @param int $page_number Blog archive page number.
	 * @return string
	 */
	public function get_blog_pagination_url( $page_number ) {
		return $this->get_post_type_archive_pagination_url( 'post', $page_number );
	}

	/**
	 * Backward-compatible wrapper for blog paginated URLs.
	 *
	 * @param int $offset Batch offset.
	 * @param int $limit  Batch size.
	 * @return array<int, string>
	 */
	public function get_blog_pagination_urls( $offset = 0, $limit = 0 ) {
		return $this->get_post_type_archive_pagination_urls( 'post', $offset, $limit );
	}

	/**
	 * Backward-compatible wrapper for blog pagination cleanup.
	 */
	public function cleanup_stale_blog_pagination() {
		$this->cleanup_stale_post_type_pagination( 'post' );
	}

	/**
	 * Backward-compatible wrapper for blog pagination regeneration.
	 *
	 * @param int $offset Batch offset.
	 * @param int $limit  Batch size.
	 * @return array<int, bool|string>
	 */
	public function regenerate_blog_pagination( $offset = 0, $limit = 0 ) {
		return $this->regenerate_post_type_pagination( 'post', $offset, $limit );
	}

	/**
	 * Backward-compatible wrapper for blog archive regeneration.
	 */
	public function regenerate_blog_archives() {
		$this->regenerate_post_type_archives( 'post' );
	}

	/**
	 * Backward-compatible wrapper for blog archive URLs.
	 *
	 * @return array<int, string>
	 */
	public function get_blog_archive_urls() {
		return $this->get_archive_urls( 'post' );
	}

	/**
	 * Backward-compatible wrapper for delayed blog archive regeneration.
	 */
	public function schedule_blog_archive_regeneration() {
		$this->schedule_archive_regeneration( 'post' );
	}

	/**
	 * Generate static HTML by URL.
	 */
	public function generate_html_by_url( $url ) {
	WPSH_Logger::info( 'Starting static HTML generation', array( 'url' => $url ) );
		if ( ! $url ) {
			WPSH_Logger::error( 'Empty URL provided for generation' );
			return 'Empty URL';
		}

		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		// Safety: Do not generate if this URL belongs to an excluded post.
		$post_id = url_to_postid( $url );
		if ( $post_id ) {
			if ( WPSH_Helper::is_excluded( $post_id ) ) {
				WPSH_Logger::warning( 'Skipping generation - post is excluded', array( 'url' => $url, 'post_id' => $post_id ) );
				return 'Post is excluded from static cache';
			}
			if ( get_post_status( $post_id ) !== 'publish' ) {
				WPSH_Logger::warning( 'Skipping generation - post not published', array( 'url' => $url, 'post_id' => $post_id, 'status' => get_post_status($post_id) ) );
				return 'Post is not published';
			}
		}

		$request_url = $url;
		$parsed = wp_parse_url( $url );
		$default_timeout = 30;
		if ( function_exists( 'wp_doing_ajax' ) && wp_doing_ajax() ) {
			$default_timeout = 20;
		} elseif ( function_exists( 'wp_doing_cron' ) && wp_doing_cron() ) {
			$default_timeout = 45;
		}
		$timeout = max( 10, (int) apply_filters( 'wpsh_generation_request_timeout', $default_timeout, $url ) );
		$args = array(
			'timeout'   => $timeout,
			'sslverify' => false,
			'cookies'   => array()
		);
		
		$host = isset( $parsed['host'] ) ? $parsed['host'] : '';
		
		// OPTIONAL: Loopback bypass mechanism. 
		// If the host matches the local home_url host, we can try to use 127.0.0.1 
		// to bypass DNS/routing issues, while keeping the Host header.
		$home_url_host = wp_parse_url( home_url(), PHP_URL_HOST );
		if ( $host === $home_url_host ) {
			$request_url = str_replace( $host, '127.0.0.1', $url );
		}
		
		$args['headers'] = array( 
			'Host'          => $host,
			'Cache-Control' => 'no-cache',
			'Pragma'        => 'no-cache',
			'Connection'    => 'close',
			'X-WPSH-Skip'   => '1',
		);
		$args['sslverify'] = false;
		$args['redirection'] = 5;

		$response = wp_remote_get( $request_url, $args );

		if ( is_wp_error( $response ) ) {
			WPSH_Logger::error( 'HTTP request failed', array( 'url' => $request_url, 'error' => $response->get_error_message() ) );
			// Fallback: If loopback failed (e.g. 127.0.0.1:443 refused), try the original URL.
			if ( $request_url !== $url ) {
				WPSH_Logger::info( 'Trying fallback to original URL', array( 'url' => $url ) );
				$response = wp_remote_get( $url, $args );
			}
			
			if ( is_wp_error( $response ) ) {
				WPSH_Logger::error( 'Fallback request also failed', array( 'url' => $url, 'error' => $response->get_error_message() ) );
				return $response->get_error_message();
			}
		}
		
		$code = wp_remote_retrieve_response_code( $response );
		WPSH_Logger::debug( 'HTTP response received', array( 'url' => $request_url, 'status_code' => $code ) );
		if ( $code !== 200 ) {
			WPSH_Logger::warning( 'Non-200 status code received', array( 'url' => $request_url, 'status_code' => $code ) );
			// Second Fallback: If loopback returned a non-200 code, try the original URL once.
			if ( $request_url !== $url ) {
				WPSH_Logger::info( 'Trying fallback for non-200 status', array( 'url' => $url ) );
				$response = wp_remote_get( $url, $args );
				$code     = wp_remote_retrieve_response_code( $response );
			}
			
			if ( $code !== 200 ) {
				WPSH_Logger::error( 'Failed to get 200 status code', array( 'url' => $url, 'final_status' => $code ) );
				return 'HTTP Error ' . $code;
			}
		}

		$html = wp_remote_retrieve_body( $response );
		if ( empty( $html ) ) {
			WPSH_Logger::warning( 'Empty response body received', array( 'url' => $request_url ) );
			// Final Fallback for empty body.
			if ( $request_url !== $url ) {
				WPSH_Logger::info( 'Trying fallback for empty body', array( 'url' => $url ) );
				$response = wp_remote_get( $url, $args );
				$html     = wp_remote_retrieve_body( $response );
			}
			if ( empty( $html ) ) {
				WPSH_Logger::error( 'Failed to get response body', array( 'url' => $url ) );
				return 'Empty Response Body';
			}
		}

		$html = str_replace( array( "\r\n", "\r" ), "\n", (string) $html );

		// Some themes/plugins emit stray content after the closing HTML tag.
		// Strip anything outside the actual document so it does not show at the bottom of the page.
		if ( preg_match( '/(<\/html\s*>).*/is', $html ) ) {
			$html = preg_replace( '/(<\/html\s*>).*/is', '$1', $html, 1 );
		} elseif ( preg_match( '/(<\/body\s*>).*/is', $html ) ) {
			$html = preg_replace( '/(<\/body\s*>).*/is', '$1', $html, 1 );
		}
		
		$date_format   = get_option( 'date_format' ) . ' ' . get_option( 'time_format' );
		$initial_time_ts = current_time( 'timestamp' );
		$time_str        = date_i18n( $date_format, $initial_time_ts );
		$cache_comment   = '<!-- Served from Moon Static HTML cache generated at: ' . $time_str . ' -->';

		if ( stripos( $html, 'Moon Static HTML cache generated at:' ) !== false ) {
			$html = preg_replace( '/\s*<!--\s*Served from Moon Static HTML cache generated at:.*?-->\s*/i', "\n", $html, 1 );
		}

		if ( preg_match( '/<\/body>/i', $html ) ) {
			$html = preg_replace( '/<\/body>/i', "\n" . $cache_comment . "\n</body>", $html, 1 );
		} elseif ( preg_match( '/<\/html>/i', $html ) ) {
			$html = preg_replace( '/<\/html>/i', "\n" . $cache_comment . "\n</html>", $html, 1 );
		} else {
			$html = rtrim( $html ) . "\n" . $cache_comment;
		}
		
		$path = $this->get_static_path_from_url( $url );
		if ( ! $path ) return 'Invalid Path';
		
		$dir = dirname( $path );
		if ( ! $wp_filesystem->exists( $dir ) ) {
			wp_mkdir_p( $dir );
		}
		
		// WRITE ONLY IF SUCCESSFUL (SOFT OVERWRITE).
		if ( ! $wp_filesystem->put_contents( $path, $html ) ) {
			WPSH_Logger::error( 'Failed to write static file', array( 'path' => $path, 'url' => $url ) );
			return 'Could not write file (WP_Filesystem): ' . $path;
		}

		// IMPORTANT: Align the injected timestamp with the real filesystem mtime.
		// This prevents "timestamp mismatch" between:
		// - the HTML comment timestamp, and
		// - the admin "Generated at" timestamp (which uses filemtime()).
		$mtime_ts = false;
		if ( function_exists( 'filemtime' ) ) {
			$mtime_ts = @filemtime( $path );
		}
		if ( ( false === $mtime_ts || null === $mtime_ts ) && method_exists( $wp_filesystem, 'mtime' ) ) {
			$mtime_ts = $wp_filesystem->mtime( $path );
		}

		if ( $mtime_ts && is_numeric( $mtime_ts ) ) {
			$mtime_ts    = (int) $mtime_ts;
			$mtime_time_str  = date_i18n( $date_format, $mtime_ts );
			$mtime_comment   = '<!-- Served from Moon Static HTML cache generated at: ' . $mtime_time_str . ' -->';

			if ( $mtime_comment !== $cache_comment ) {
				// Replace the first matching cache comment in the generated HTML.
				$html = preg_replace(
					'/<!--\s*Served from Moon Static HTML cache generated at:.*?-->\s*/i',
					$mtime_comment,
					(string) $html,
					1
				);

				// Best effort: if update fails, keep the first write.
				$wp_filesystem->put_contents( $path, $html );
				$cache_comment = $mtime_comment;
			}
		}

		WPSH_Logger::success( 'Static file generated successfully', array( 'path' => $path, 'url' => $url, 'size' => strlen( $html ) ) );
		
		return true;
	}

	/**
	 * Regenerate on post save (WP 5.6+).
	 */
	public function regenerate_on_save( $post_id, $post, $update, $post_before ) {
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) return;
		
		if ( $post->post_status !== 'publish' ) {
			$post_type = isset( $post->post_type ) ? (string) $post->post_type : '';

			$url = get_permalink( $post_id );
			$path = $this->get_static_path_from_url( $url );

			global $wp_filesystem;
			if ( empty( $wp_filesystem ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
				WP_Filesystem();
			}

			if ( $path && $wp_filesystem->exists( $path ) ) {
				WPSH_Logger::deleted(
					'Static file removed due to unpublished status (Post Type: ' . $post_type . ')',
					array(
						'path'    => $path,
						'post_id' => $post_id,
						'url'     => $url,
						'reason'  => 'post_unpublished',
					)
				);
				$wp_filesystem->delete( $path );
			}

			// Performance: queue archive/pagination regeneration only.
			// The per-post static HTML deletion above must happen immediately,
			// but regenerating archives/pagination synchronously slows down "Save".
			if ( ! empty( $post_type ) && $this->post_type_has_archive_support( $post_type ) ) {
				$this->schedule_archive_regeneration( $post_type );
			}

			// Update any mapped custom listing page(s) even if the CPT has no archive endpoint.
			if ( ! empty( $post_type ) ) {
				$this->schedule_cpt_listing_page_regeneration( $post_type );
			}
			return;
		}

		$post_type = $post->post_type;
		if ( ! in_array( $post_type, get_post_types( array( 'public' => true ) ) ) ) return;

		$url = get_permalink( $post_id );
		$path = $this->get_static_path_from_url( $url );
		
		// Handle Exclusion.
		if ( WPSH_Helper::is_excluded( $post_id ) ) {
			global $wp_filesystem;
			if ( empty( $wp_filesystem ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
				WP_Filesystem();
			}

			if ( $path && $wp_filesystem->exists( $path ) ) {
				WPSH_Logger::deleted(
					'Static file removed due to exclusion (Post Type: ' . $post_type . ')',
					array(
						'path'   => $path,
						'post_id'=> $post_id,
						'url'    => $url,
						'reason' => 'post_excluded',
					)
				);
				$wp_filesystem->delete( $path );
			}

			// Performance: queue archive/pagination regeneration only.
			if ( $this->post_type_has_archive_support( $post_type ) ) {
				$this->schedule_archive_regeneration( $post_type );
			}
			return;
		}

		// Preserve the current static file until a fresh version is generated successfully.
		$result = $this->generate_html_by_url( $url );
		if ( true !== $result ) {
			WPSH_Logger::warning(
				'Static regeneration failed; keeping existing file in place',
				array(
					'post_id' => $post_id,
					'url'     => $url,
					'error'   => $result,
				)
			);
		}

		// If the post type has an archive, also regenerate its listing pages and pagination.
		// Performance: queue archive/pagination regeneration only.
		if ( $this->post_type_has_archive_support( $post_type ) ) {
			$this->schedule_archive_regeneration( $post_type );
		}

		// Update mapped custom listing pages for CPTs without archive endpoints.
		$this->schedule_cpt_listing_page_regeneration( $post_type );
	}

	/**
	 * Fallback for older WP versions.
	 */
	public function save_post_fallback( $post_id ) {
		if ( did_action( 'wp_after_insert_post' ) ) return;
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) return;
		
		$post_type = get_post_type( $post_id );
		if ( ! in_array( $post_type, get_post_types( array( 'public' => true ) ) ) ) return;
		if ( get_post_status( $post_id ) !== 'publish' ) {
			if ( $this->post_type_has_archive_support( $post_type ) ) {
				$this->schedule_archive_regeneration( $post_type );
			}
			$this->schedule_cpt_listing_page_regeneration( $post_type );
			return;
		}

		$url = get_permalink( $post_id );
		$path = $this->get_static_path_from_url( $url );

		$result = $this->generate_html_by_url( $url );
		if ( true !== $result ) {
			WPSH_Logger::warning(
				'Fallback regeneration failed; keeping existing file in place',
				array(
					'post_id' => $post_id,
					'url'     => $url,
					'error'   => $result,
				)
			);
		}
		if ( $this->post_type_has_archive_support( $post_type ) ) {
			$this->schedule_archive_regeneration( $post_type );
		}
		$this->schedule_cpt_listing_page_regeneration( $post_type );
	}

	/**
	 * Regenerate on term save.
	 */
	public function regenerate_on_term_save( $term_id, $tt_id, $taxonomy ) {
		$excluded_tax = (array) get_option( 'wpsh_excluded_taxonomies', array() );
		if ( in_array( $taxonomy, $excluded_tax, true ) ) return;
		
		$term = get_term( $term_id, $taxonomy );
		if ( $term && ! is_wp_error( $term ) ) {
			$url = get_term_link( $term );
			if ( ! is_wp_error( $url ) ) {
				$result = $this->generate_html_by_url( $url );
				if ( true !== $result ) {
					WPSH_Logger::warning(
						'Term static regeneration failed; keeping existing file in place',
						array(
							'term_id'  => $term_id,
							'taxonomy' => $taxonomy,
							'url'      => $url,
							'error'    => $result,
						)
					);
				}
			}
			$this->generate_html_by_url( home_url( '/' ) );
		}
	}

	/**
	 * Delete static on post removal.
	 */
	public function delete_static_on_remove( $post_id ) {
		if ( wp_is_post_revision( $post_id ) ) return;
		
		$url = get_permalink( $post_id );
		$path = $this->get_static_path_from_url( $url );

		global $wp_filesystem;
		if ( empty( $wp_filesystem ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
			WP_Filesystem();
		}

		$post_type = get_post_type( $post_id );
		
		if ( $path && $wp_filesystem->exists( $path ) ) {
			WPSH_Logger::deleted(
				'Static file removed due to post deletion (Post Type: ' . $post_type . ')',
				array(
					'path'   => $path,
					'post_id'=> $post_id,
					'url'    => $url,
					'reason' => 'post_deleted',
				)
			);
			$wp_filesystem->delete( $path );
		}

		if ( $this->post_type_has_archive_support( $post_type ) ) {
			$this->schedule_archive_regeneration( $post_type );
		}

		// Update mapped custom listing pages for CPTs without archive endpoints.
		$this->schedule_cpt_listing_page_regeneration( $post_type );
	}
}
