<?php

namespace QuantumCache\Cache;
\defined( 'ABSPATH' ) || exit;
use QuantumCache\Support\StoreFactory;

class FragmentCache {


	private static function capture_producer( callable $producer ): string {
		$level = ob_get_level();
		ob_start();
		try {
			$out = call_user_func( $producer );
			$buf = ob_get_clean(); // closes the buffer we started
			return ( null !== $out ) ? (string) $out : (string) $buf;
		} finally {
			// If an exception occurred before ob_get_clean(), close ONLY our buffer.
			if ( ob_get_level() > $level ) {
				// Close only the buffer we opened if it's still active.
				$ok = ob_end_clean();
				if ( false === $ok ) {
					// If another component interfered with buffering, fail silently.
					// (We do not want warnings in front-end rendering.)
				}
			}
		}
	}



	public static function peek( string $name, array $ctx ): ?string {
		$no_cache = (bool) filter_input( INPUT_GET, 'qc_nocache', FILTER_VALIDATE_BOOLEAN );
		if ( $no_cache ) {
			return null;
		}

		$ctx['name'] = $name;
		$ctx['lang'] = function_exists( 'get_locale' ) ? get_locale() : 'en_US';
		$ctx['role'] = 'anon';
		if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
			if ( function_exists( 'quantumcache_allow_logged_in_cache' ) && quantumcache_allow_logged_in_cache() ) {
				$user = function_exists( 'wp_get_current_user' ) ? wp_get_current_user() : null;
				if ( $user && is_object( $user ) ) {
					$roles_arr   = array_map( 'strval', (array) $user->roles );
					sort( $roles_arr );
					$ctx['role'] = implode( ',', $roles_arr );
				}
			}
		}

		$k   = self::key( 'frag', $ctx );
		$raw = StoreFactory::get()->get( $k );
		return ( null !== $raw ) ? (string) $raw : null;
	}

	public static function render( string $name, array $ctx, callable $producer, array $opts = array() ): string {
		// 1) Debug bypass via query string (read-only; no DB writes).
		$no_cache = (bool) filter_input( INPUT_GET, 'qc_nocache', FILTER_VALIDATE_BOOLEAN );
        // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only flag; does not cause state change or persist data.
		if ( $no_cache ) {
					return self::capture_producer( $producer );

		}

		$ttl       = (int) ( $opts['ttl'] ?? 600 );
		$stale_ttl = (int) ( $opts['stale_ttl'] ?? 86400 );
		// Allow global defaults to be overridable from admin options.
		$ttl       = (int) apply_filters( 'quantumcache_fragment_ttl', $ttl, $name, $ctx ?? array() );
		$stale_ttl = (int) apply_filters( 'quantumcache_fragment_stale_ttl', $stale_ttl, $name, $ctx ?? array() );
		$tags      = (array) ( $opts['tags'] ?? array() );

		$ctx['name'] = $name;
		$ctx['lang'] = function_exists( 'get_locale' ) ? get_locale() : 'en_US';

		$ctx['role'] = 'anon';
		if ( function_exists( 'is_user_logged_in' ) && is_user_logged_in() ) {
			if ( function_exists( 'quantumcache_allow_logged_in_cache' ) && quantumcache_allow_logged_in_cache() ) {
				$user = function_exists( 'wp_get_current_user' ) ? wp_get_current_user() : null;
				if ( $user && is_object( $user ) ) {
					$roles_arr   = array_map( 'strval', (array) $user->roles );
					sort( $roles_arr );
					$ctx['role'] = implode( ',', $roles_arr );
				}
			}
		}

		$k = self::key( 'frag', $ctx );

		// Light debug header with last 16 chars of the key; avoid header injection by construction.
		\QuantumCache\quantumcache_note_fragment_key( $k );

		$raw = StoreFactory::get()->get( $k );
		if ( null !== $raw ) { // Yoda
			do_action( 'quantumcache_cache_hit', 'fragment' );
			return (string) $raw;
		}

		$lock_key = 'frag:' . substr( sha1( $k ), 0, 12 ); // snake_case
				\QuantumCache\quantumcache_send_debug_header( 'X-QC-Frag-Render', '1' );
		$locked = StoreFactory::get()->acquireLock( $lock_key, 30000 );
		if ( true === $locked ) {
			try {
								$html = self::capture_producer( $producer );

				// 3) Nonce-bearing fragments & JWT-like tokens are not cached
				$has_nonce                      = (bool) preg_match( '/(^|[^A-Za-z0-9_])_wpnonce($|[^A-Za-z0-9_])/i', $html );
								$looks_like_jwt = (bool) preg_match(
									'/\b[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\.[A-Za-z0-9_\-]{10,}\b/',
									$html
								);

				if ( ! $has_nonce && ! $looks_like_jwt ) {
					StoreFactory::get()->set( $k, $html, $ttl, $stale_ttl );
					$_ctx        = array(
						'surface' => 'fragment',
						'name'    => (string) $name,
					) + (array) $ctx;
					$_final_tags = (array) apply_filters( 'quantumcache_store_additional_tags', (array) $tags, $_ctx );

					if ( $_final_tags ) {
						StoreFactory::get()->addTags( $k, $_final_tags );

						// Accumulate tags used during this request (optional, harmless, needed for Pro)
						$GLOBALS['quantumcache_request_tags'] = array_values(
							array_unique(
								array_merge(
									(array) ( $GLOBALS['quantumcache_request_tags'] ?? array() ),
									array_values( array_filter( $_final_tags, 'is_string' ) )
								)
							)
						);
					}
					do_action( 'quantumcache_cache_store', 'fragment' );
				} else {
					do_action( 'quantumcache_cache_miss', 'fragment' );
				}
				return $html;
			} finally {
				StoreFactory::get()->releaseLock( $lock_key );
			}
		}

			do_action( 'quantumcache_cache_miss', 'fragment' );
			return self::capture_producer( $producer );
	}

	private static function site_prefix(): string {
		return StoreFactory::sitePrefix();
	}

	private static function key( string $type, array $ctx ): string {
		ksort( $ctx );
				$json = function_exists( 'wp_json_encode' ) ? wp_json_encode( $ctx ) : json_encode( $ctx );
				$fp   = substr( sha1( (string) $json ), 0, 16 );
		return self::site_prefix() . $type . ':' . $fp;
	}
}
