<?php
/**
 * BaseCloud Boost — .htaccess Manager
 *
 * @package BaseCloud\Boost\Compatibility
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Compatibility;

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

/**
 * Manages .htaccess rules for Apache servers.
 */
class Htaccess_Manager {

	private const MARKER_START = '# BEGIN BaseCloud Boost';
	private const MARKER_END   = '# END BaseCloud Boost';

	/**
	 * Add BaseCloud Boost rules to .htaccess.
	 *
	 * @return bool True on success.
	 */
	public function add_rules(): bool {
		if ( $this->is_nginx() ) {
			return false;
		}

		$htaccess = get_home_path() . '.htaccess';
		if ( ! file_exists( $htaccess ) || ! is_writable( $htaccess ) ) {
			return false;
		}

		$this->remove_rules(); // Remove existing block first.

		$current = file_get_contents( $htaccess );
		$rules   = self::MARKER_START . "\n" . $this->get_rules() . self::MARKER_END . "\n\n";

		file_put_contents( $htaccess, $rules . $current );
		return true;
	}

	/**
	 * Remove BaseCloud Boost rules from .htaccess.
	 *
	 * @return bool True on success.
	 */
	public function remove_rules(): bool {
		$htaccess = get_home_path() . '.htaccess';
		if ( ! file_exists( $htaccess ) ) {
			return false;
		}

		$content = file_get_contents( $htaccess );
		$pattern = '/' . preg_quote( self::MARKER_START, '/' ) . '.*?' . preg_quote( self::MARKER_END, '/' ) . '\n*/s';
		$content = preg_replace( $pattern, '', $content );

		file_put_contents( $htaccess, $content );
		return true;
	}

	/**
	 * Get the .htaccess rules string.
 	 *
	 * @return string
	 */
	public function get_rules(): string {
		// Variance features change which cache file a request must receive. Pure
		// mod_rewrite can't compute a per-cookie/per-country variant key, so when
		// those are in play we route the request through PHP (advanced-cache.php),
		// which resolves the correct variant. Mobile is the one variant Apache CAN
		// serve directly (User-Agent match), so we keep that fast path.
		$vary_country = (bool) BCBOOST_get_option( 'cache_vary_by_country', false );
		$mobile_sep   = (bool) BCBOOST_get_option( 'cache_mobile_separate', true );

		// Cookies that must force a PHP-served variant (currency / geo switchers).
		$cookie_bypass = [
			'aelia_cs_selected_currency', 'aelia_customer_country', 'woocommerce_currency',
			'woocs_current_currency', 'wmc_current_currency', 'wcml_currency', 'wcpbc_country',
			'bcboost_country',
		];
		foreach ( array_filter( array_map( 'trim', explode( "\n", (string) BCBOOST_get_option( 'vary_cookies', '' ) ) ) ) as $ck ) {
			$ck = preg_replace( '/[^A-Za-z0-9_\-]/', '', $ck );
			if ( '' !== $ck && ! in_array( $ck, $cookie_bypass, true ) ) {
				$cookie_bypass[] = $ck;
			}
		}
		$cookie_regex = implode( '|', $cookie_bypass );

		$rules  = "\n# ── BaseCloud Boost: Serve Pre-compressed Cache Files ─────────────────\n";
		$rules .= "<IfModule mod_rewrite.c>\nRewriteEngine On\n\n";
		$rules .= "# Flag requests that must bypass cache.\n";
		$rules .= "# Using [E=BCBOOST_BYPASS:1] instead of [L] so WordPress's own rewrite rules\n";
		$rules .= "# (below this block in .htaccess) still run — [L] would stop them and cause\n";
		$rules .= "# 404s for logged-in users (admins, Elementor) on any non-root URL.\n";
		$rules .= "RewriteCond %{HTTP_COOKIE} wordpress_logged_in [NC]\nRewriteRule ^ - [E=BCBOOST_BYPASS:1]\n\n";
		$rules .= "RewriteCond %{REQUEST_METHOD} POST\nRewriteRule ^ - [E=BCBOOST_BYPASS:1]\n\n";
		$rules .= "RewriteCond %{QUERY_STRING} .\nRewriteRule ^ - [E=BCBOOST_BYPASS:1]\n\n";

		// Currency/geo cookie → let PHP serve the correct variant.
		$rules .= "# Currency / geolocation cookies: let PHP resolve the correct cached variant.\n";
		$rules .= "RewriteCond %{HTTP_COOKIE} (" . $cookie_regex . ") [NC]\nRewriteRule ^ - [E=BCBOOST_BYPASS:1]\n\n";

		if ( $vary_country ) {
			// No reliable country in mod_rewrite → always hand HTML to PHP.
			$rules .= "# Per-country caching is enabled: PHP (advanced-cache.php) serves all variants.\n";
			$rules .= "RewriteRule ^ - [E=BCBOOST_BYPASS:1]\n\n";
		}

		$rules .= "# Detect scheme — default http, upgrade to https when applicable.\n";
		$rules .= "# Checks both %{HTTPS} (direct SSL) and X-Forwarded-Proto (reverse proxy /\n";
		$rules .= "# CloudWays Nginx front-end). Cache files are stored under the scheme\n";
		$rules .= "# subdirectory, so this must match what Page_Cache::url_to_cache_file() writes.\n";
		$rules .= "RewriteRule ^ - [E=BCBOOST_SCHEME:http]\n";
		$rules .= "RewriteCond %{HTTPS} on [OR]\nRewriteCond %{HTTP:X-Forwarded-Proto} https\n";
		$rules .= "RewriteRule ^ - [E=BCBOOST_SCHEME:https]\n\n";

		// Device variant (matches advanced-cache.php / Cache_Context mobile UA list).
		$rules .= "RewriteRule ^ - [E=BCBOOST_DEVICE:]\n";
		if ( $mobile_sep ) {
			$rules .= "# Separate mobile cache: serve index-mobile.html to mobile User-Agents.\n";
			$rules .= "RewriteCond %{HTTP_USER_AGENT} (Mobile|Android|Silk/|Kindle|BlackBerry|Opera\\ Mini|Opera\\ Mobi|webOS) [NC]\n";
			$rules .= "RewriteRule ^ - [E=BCBOOST_DEVICE:-mobile]\n";
		}
		$rules .= "\n";

		$base = '/wp-content/cache/basecloud-boost/%{ENV:BCBOOST_SCHEME}/%{HTTP_HOST}%{REQUEST_URI}/index%{ENV:BCBOOST_DEVICE}.html';

		$rules .= "# Serve Brotli cache\n";
		$rules .= "RewriteCond %{ENV:BCBOOST_BYPASS} !1\nRewriteCond %{HTTP:Accept-Encoding} br\n";
		$rules .= "RewriteCond %{DOCUMENT_ROOT}{$base}.br -f\n";
		$rules .= "RewriteRule .* {$base}.br [L,E=no-gzip:1,E=no-brotli:0]\n\n";

		$rules .= "# Serve GZIP cache\n";
		$rules .= "RewriteCond %{ENV:BCBOOST_BYPASS} !1\nRewriteCond %{HTTP:Accept-Encoding} gzip\n";
		$rules .= "RewriteCond %{DOCUMENT_ROOT}{$base}.gz -f\n";
		$rules .= "RewriteRule .* {$base}.gz [L,E=no-gzip:1]\n\n";

		$rules .= "# Serve plain cache\n";
		$rules .= "RewriteCond %{ENV:BCBOOST_BYPASS} !1\n";
		$rules .= "RewriteCond %{DOCUMENT_ROOT}{$base} -f\n";
		$rules .= "RewriteRule .* {$base} [L]\n";
		$rules .= "</IfModule>\n\n";

		$rules .= "# ── BaseCloud Boost: Headers for cached files ──────────────────────────\n";
		$rules .= "<FilesMatch \"index[\\w-]*\\.html\\.br$\">\n";
		$rules .= "    Header set Content-Encoding \"br\"\n";
		$rules .= "    Header set Vary \"Accept-Encoding\"\n";
		$rules .= "    Header set Content-Type \"text/html; charset=UTF-8\"\n";
		$rules .= "    Header set X-BaseCloud-Boost \"HIT\"\n";
		$rules .= "    Header set Cache-Control \"public, max-age=0, must-revalidate\"\n";
		$rules .= "</FilesMatch>\n\n";
		$rules .= "<FilesMatch \"index[\\w-]*\\.html\\.gz$\">\n";
		$rules .= "    Header set Content-Encoding \"gzip\"\n";
		$rules .= "    Header set Vary \"Accept-Encoding\"\n";
		$rules .= "    Header set Content-Type \"text/html; charset=UTF-8\"\n";
		$rules .= "    Header set X-BaseCloud-Boost \"HIT\"\n";
		$rules .= "    Header set Cache-Control \"public, max-age=0, must-revalidate\"\n";
		$rules .= "</FilesMatch>\n\n";
		$rules .= "<FilesMatch \"index[\\w-]*\\.html$\">\n";
		$rules .= "    Header set X-BaseCloud-Boost \"HIT\"\n";
		$rules .= "    Header set Cache-Control \"public, max-age=0, must-revalidate\"\n";
		$rules .= "</FilesMatch>\n";

		$rules .= '
# ── BaseCloud Boost: GZIP Compression ─────────────────────────────────
<IfModule mod_deflate.c>
    AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript
    AddOutputFilterByType DEFLATE application/javascript application/x-javascript
    AddOutputFilterByType DEFLATE application/json application/xml
    AddOutputFilterByType DEFLATE image/svg+xml font/woff font/woff2
</IfModule>
';

		// The "Browser Cache Headers" toggle gates ONLY the static-asset caching
		// rules below. The HTML Cache-Control blocks above (and
		// Browser_Cache::add_cache_headers()) are correctness headers and are
		// never optional — without them browsers heuristic-cache HTML and serve
		// stale pages no server-side purge can reach.
		if ( ! BCBOOST_get_option( 'browser_cache', true ) ) {
			return $rules . "\n";
		}

		// Asset Cache-Control values come from the single guarded source of
		// truth (Browser_Cache::asset_cache_control()), which enforces the
		// CLAUDE.md §6.1 invariant: `immutable` only ever on content-hashed
		// filenames, never by bare .css/.js extension.
		$cc_media         = \BaseCloud\Boost\Core\Browser_Cache::asset_cache_control( 'media' );
		$cc_assets        = \BaseCloud\Boost\Core\Browser_Cache::asset_cache_control( 'assets' );
		$cc_fingerprinted = \BaseCloud\Boost\Core\Browser_Cache::asset_cache_control( 'fingerprinted' );
		$fp_pattern       = \BaseCloud\Boost\Core\Browser_Cache::FINGERPRINT_FILE_PATTERN;

		return $rules . '
# ── BaseCloud Boost: Browser Caching ──────────────────────────────────
<IfModule mod_expires.c>
    ExpiresActive On
    ExpiresByType image/jpeg        "access plus 1 year"
    ExpiresByType image/gif         "access plus 1 year"
    ExpiresByType image/png         "access plus 1 year"
    ExpiresByType image/webp        "access plus 1 year"
    ExpiresByType image/avif        "access plus 1 year"
    ExpiresByType image/svg+xml     "access plus 1 year"
    ExpiresByType image/x-icon      "access plus 1 year"
    ExpiresByType text/css          "access plus 1 month"
    ExpiresByType text/javascript   "access plus 1 month"
    ExpiresByType application/javascript "access plus 1 month"
    ExpiresByType font/woff         "access plus 1 year"
    ExpiresByType font/woff2        "access plus 1 year"
    ExpiresByType application/font-woff  "access plus 1 year"
    ExpiresByType application/font-woff2 "access plus 1 year"
</IfModule>

# CSS/JS keep the same URL when a theme/plugin file is edited, so they must
# revalidate — a long/immutable browser lifetime here is exactly the
# "I cleared the cache but visitors still see the old site" bug. Only
# BaseCloud Boost\'s content-fingerprinted minify output (md5 of the content
# in the filename — bytes can never change at that URL) is safe to mark
# immutable. Order matters: fingerprinted files match both blocks and the
# last "Header set" wins. ETags are intentionally left enabled.
<IfModule mod_headers.c>
    <FilesMatch "\.(ico|jpg|jpeg|png|gif|webp|avif|svg|woff|woff2|ttf|eot)$">
        Header set Cache-Control "' . $cc_media . '"
    </FilesMatch>
    <FilesMatch "\.(css|js)$">
        Header set Cache-Control "' . $cc_assets . '"
    </FilesMatch>
    <FilesMatch "' . $fp_pattern . '">
        Header set Cache-Control "' . $cc_fingerprinted . '"
    </FilesMatch>
</IfModule>

';
	}

	/**
	 * Check if the server is nginx.
	 *
	 * @return bool
	 */
	public function is_nginx(): bool {
		$software = $_SERVER['SERVER_SOFTWARE'] ?? '';
		return stripos( $software, 'nginx' ) !== false;
	}

	/**
	 * Get equivalent nginx configuration snippet.
	 *
	 * @return string
	 */
	public function get_nginx_rules(): string {
		$vary_country = (bool) BCBOOST_get_option( 'cache_vary_by_country', false );
		$mobile_sep   = (bool) BCBOOST_get_option( 'cache_mobile_separate', false );

		// Cookies that must bypass the static cache (logged-in / WooCommerce /
		// currency-geo switchers). Mirrors advanced-cache.php + Cache_Context.
		$cookies = [
			'wordpress_logged_in', 'wp-postpass', 'woocommerce_items_in_cart',
			'woocommerce_cart_hash', 'wp_woocommerce_session',
			'aelia_cs_selected_currency', 'aelia_customer_country', 'woocommerce_currency',
			'woocs_current_currency', 'wmc_current_currency', 'wcml_currency', 'wcpbc_country',
			'bcboost_country',
		];
		foreach ( array_filter( array_map( 'trim', explode( "\n", (string) BCBOOST_get_option( 'vary_cookies', '' ) . "\n" . (string) BCBOOST_get_option( 'exclude_cookies', '' ) ) ) ) as $ck ) {
			$ck = preg_replace( '/[^A-Za-z0-9_\-]/', '', $ck );
			if ( '' !== $ck && ! in_array( $ck, $cookies, true ) ) {
				$cookies[] = $ck;
			}
		}
		$cookie_regex = implode( '|', $cookies );

		$out  = "# BaseCloud Boost — nginx cache serving rules\n";
		$out .= "# Add inside your server {} block. Requires the pre-compressed siblings to be\n";
		$out .= "# served with correct headers — enable these in your http/server block:\n";
		$out .= "#     gzip_static on;\n";
		$out .= "#     brotli_static on;   # only if your nginx has the ngx_brotli module\n";
		$out .= "# nginx then serves the .gz/.br copy automatically with Content-Encoding set.\n\n";

		if ( $vary_country ) {
			$out .= "# NOTE: \"Separate Cache per Country\" is ENABLED. nginx cannot compute the\n";
			$out .= "# per-country variant, so these rules intentionally send page requests to PHP\n";
			$out .= "# (advanced-cache.php), which serves the correct per-country/-cookie variant.\n";
			$out .= "# Leave the block below as-is — it skips the static fast-path for HTML.\n\n";
		}

		$out .= "set \$bcboost_skip 0;\n";
		$out .= "set \$bcboost_device \"\";\n\n";
		$out .= "if (\$request_method != GET)  { set \$bcboost_skip 1; }   # non-GET\n";
		$out .= "if (\$is_args)                { set \$bcboost_skip 1; }   # any query string\n";
		$out .= "if (\$http_cookie ~* \"(" . $cookie_regex . ")\") { set \$bcboost_skip 1; }\n";
		if ( $vary_country ) {
			$out .= "set \$bcboost_skip 1;   # per-country caching → always serve via PHP\n";
		}
		$out .= "\n";

		if ( $mobile_sep ) {
			$out .= "# Separate mobile cache.\n";
			$out .= "if (\$http_user_agent ~* \"(Mobile|Android|Silk/|Kindle|BlackBerry|Opera\\ Mini|Opera\\ Mobi|webOS)\") { set \$bcboost_device \"-mobile\"; }\n\n";
		}

		$out .= "location / {\n";
		$out .= "    # Conditionally skip the static cache (the 418 trick avoids \"if is evil\"\n";
		$out .= "    # problems with try_files).\n";
		$out .= "    error_page 418 = @bcboost_miss;\n";
		$out .= "    if (\$bcboost_skip = 1) { return 418; }\n";
		$out .= "    try_files /wp-content/cache/basecloud-boost/\$scheme/\$host\$uri/index\$bcboost_device.html @bcboost_miss;\n";
		$out .= "}\n\n";
		$out .= "location @bcboost_miss {\n";
		$out .= "    try_files \$uri \$uri/ /index.php?\$args;\n";
		$out .= "}\n";

		return $out;
	}
}
