<?php
/**
 * Static Cache Manager for Easy Social Share Buttons
 *
 * Combines and caches CSS/JS assets for improved performance.
 * Provides automatic cache invalidation, file combining, and URL resolution.
 *
 * @package EasySocialShareButtons
 * @author  appscreo
 * @since   1.0.0
 * @version 2.0.0
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Class ESSB_Static_Cache
 *
 * High-performance static resource combining and caching for WordPress.
 *
 * Features:
 * - Combines multiple CSS/JS files into single optimized files
 * - Automatic cache invalidation on version changes
 * - SSL-aware URL handling
 * - WordPress transient-based caching
 * - File-based persistent storage
 * - Inline styles/scripts preservation
 * - Media query awareness for CSS
 * - Extensible via WordPress filters
 *
 * @since 1.0.0
 */
class ESSB_Static_Cache {

	/**
	 * Cache version for invalidation purposes.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	const CACHE_VERSION = '2.0.0';

	/**
	 * Cache directory name.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	const CACHE_DIR_NAME = 'essb-static-cache';

	/**
	 * Legacy cache directory name (for migration).
	 *
	 * @since 2.0.0
	 * @var string
	 */
	const LEGACY_CACHE_DIR_NAME = 'essb_cache_static';

	/**
	 * Singleton instance.
	 *
	 * @since 1.0.0
	 * @var ESSB_Static_Cache|null
	 */
	private static $instance = null;

	/**
	 * Array of cached handles to prevent duplicate processing.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	private $essb_cache_static_done = array();

	/**
	 * Whether CSS caching is enabled.
	 *
	 * @since 2.0.0
	 * @var bool
	 */
	private $css_cache_enabled = false;

	/**
	 * Whether JS caching is enabled.
	 *
	 * @since 2.0.0
	 * @var bool
	 */
	private $js_cache_enabled = false;

	/**
	 * Cache base directory path.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	private $cache_base_dir = '';

	/**
	 * Cache base URL.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	private $cache_base_url = '';

	/**
	 * Private constructor to enforce singleton pattern.
	 *
	 * Initializes cache settings and hooks into WordPress enqueue system.
	 *
	 * @since 1.0.0
	 */
	private function __construct() {
		// Get cache settings from options.
		$this->css_cache_enabled = essb_options_bool_value( 'essb_cache_static' );
		$this->js_cache_enabled  = essb_options_bool_value( 'essb_cache_static_js' );

		// Determine and set cache paths.
		$this->setup_cache_paths();

		// Migrate from legacy cache location if exists.
		$this->migrate_legacy_cache();

		// Hook into WordPress script/style printing.
		if ( $this->js_cache_enabled ) {
			add_filter( 'print_scripts_array', array( $this, 'init_essb_cache_static_js' ), 100 );
		}

		if ( $this->css_cache_enabled ) {
			add_filter( 'print_styles_array', array( $this, 'init_essb_cache_static_css' ), 100 );
		}
	}

	/**
	 * Get singleton instance.
	 *
	 * Creates instance if it doesn't exist, otherwise returns existing instance.
	 *
	 * @since 1.0.0
	 * @return ESSB_Static_Cache Singleton instance.
	 */
	public static function instance() {
		if ( null === self::$instance ) {
			self::$instance = new self();
		}

		return self::$instance;
	}

	/**
	 * Setup cache directory paths.
	 *
	 * Determines the best cache location with fallback support.
	 * Priority: wp-content/cache/ > wp-content/uploads/
	 *
	 * @since 2.0.0
	 *
	 * @return void
	 */
	private function setup_cache_paths() {
		// Try wp-content/cache first (WordPress standard).
		$cache_dir = WP_CONTENT_DIR . '/cache/' . self::CACHE_DIR_NAME;
		$cache_url = content_url( 'cache/' . self::CACHE_DIR_NAME );

		// Check if wp-content/cache is writable.
		$wp_content_cache = WP_CONTENT_DIR . '/cache';
		if ( ! is_dir( $wp_content_cache ) ) {
			wp_mkdir_p( $wp_content_cache );
		}

		// Test write permissions.
		if ( ! is_writable( $wp_content_cache ) ) {
			// Fallback to uploads directory.
			$upload_dir = wp_upload_dir();
			$cache_dir  = $upload_dir['basedir'] . '/' . self::CACHE_DIR_NAME;
			$cache_url  = $upload_dir['baseurl'] . '/' . self::CACHE_DIR_NAME;
		}

		$this->cache_base_dir = $cache_dir;
		$this->cache_base_url = $cache_url;
	}

	/**
	 * Migrate from legacy cache location.
	 *
	 * Checks for old cache folder (essb_cache_static in uploads)
	 * and removes it during first run.
	 *
	 * @since 2.0.0
	 *
	 * @return void
	 */
	private function migrate_legacy_cache() {
		// Check if migration already done.
		if ( get_option( 'essb_cache_static_migrated', false ) ) {
			return;
		}

		// Check for legacy cache in uploads directory.
		$upload_dir     = wp_upload_dir();
		$legacy_cache   = $upload_dir['basedir'] . '/' . self::LEGACY_CACHE_DIR_NAME;

		if ( is_dir( $legacy_cache ) ) {
			// Remove legacy cache completely.
			$this->remove_directory_recursive( $legacy_cache );
		}

		// Mark migration as complete.
		update_option( 'essb_cache_static_migrated', true, false );
	}

	/**
	 * Initialize JavaScript caching.
	 *
	 * Filters the scripts array before printing to combine ESSB scripts.
	 *
	 * @since 1.0.0
	 * @param array $todo Array of script handles to be printed.
	 * @return array Modified array of script handles.
	 */
	public function init_essb_cache_static_js( $todo ) {
		global $wp_scripts;

		if ( ! is_object( $wp_scripts ) ) {
			return $todo;
		}

		return $this->essb_cache_static_objects( $wp_scripts, $todo, 'js' );
	}

	/**
	 * Initialize CSS caching.
	 *
	 * Filters the styles array before printing to combine ESSB styles.
	 *
	 * @since 1.0.0
	 * @param array $todo Array of style handles to be printed.
	 * @return array Modified array of style handles.
	 */
	public function init_essb_cache_static_css( $todo ) {
		global $wp_styles;

		if ( ! is_object( $wp_styles ) ) {
			return $todo;
		}

		return $this->essb_cache_static_objects( $wp_styles, $todo, 'css' );
	}

	/**
	 * Process and combine static objects (scripts or styles).
	 *
	 * Main processing method that:
	 * 1. Filters items to process
	 * 2. Checks cache for existing combined file
	 * 3. Combines files if cache miss
	 * 4. Enqueues combined file
	 *
	 * @since 1.0.0
	 * @since 2.0.0 Improved performance and error handling.
	 *
	 * @param WP_Scripts|WP_Styles $object    WordPress scripts or styles object.
	 * @param array                $todo      Array of handles to process.
	 * @param string               $extension File extension ('js' or 'css').
	 * @return array Modified array of handles to process.
	 */
	public function essb_cache_static_objects( &$object, $todo, $extension ) {
		// Don't run if on admin or empty queue.
		if ( is_admin() || empty( $todo ) || ! is_array( $todo ) ) {
			return $todo;
		}

		// Allow files to be excluded from cache.
		$essb_cache_static_exclude = (array) apply_filters( 'essb_cache_static-exclude-' . $extension, array() );

		// Exclude all previously cached items.
		$essb_cache_static_exclude = array_merge( $essb_cache_static_exclude, $this->get_done() );

		// Get items that need processing.
		$essb_cache_static_todo = array_diff( $todo, $essb_cache_static_exclude );

		if ( empty( $essb_cache_static_todo ) ) {
			return $todo;
		}

		// Build cache version key.
		$cache_ver = $this->build_cache_version( $object, $essb_cache_static_todo, $extension );

		// Check if cached file already exists (no transient needed).
		$type_dir           = $this->cache_base_dir . '/static/' . $extension;
		$combined_file_path = $type_dir . '/' . $cache_ver . '.' . $extension;
		$combined_file_url  = $this->cache_base_url . '/static/' . $extension . '/' . $cache_ver . '.' . $extension;

		// If file exists, use it directly.
		if ( file_exists( $combined_file_path ) ) {
			$status = array(
				'cache_ver' => $cache_ver,
				'todo'      => $todo,
				'done'      => array_keys( $essb_cache_static_todo ), // Use todo items as done.
				'url'       => $combined_file_url,
				'file'      => $combined_file_path,
				'extension' => $extension,
			);

			$this->set_done( $cache_ver );
			return $this->essb_cache_static_enqueue_files( $object, $status );
		}

		// Process and combine files.
		$done = $this->process_files( $object, $essb_cache_static_todo, $extension );

		if ( empty( $done ) ) {
			return $todo;
		}

		// Create combined file.
		$combined_file = $this->create_combined_file( $done, $cache_ver, $extension );

		if ( false === $combined_file ) {
			return $todo;
		}

		// Build status array for enqueueing.
		$status = array(
			'cache_ver' => $cache_ver,
			'todo'      => $todo,
			'done'      => array_keys( $done ),
			'url'       => $combined_file['url'],
			'file'      => $combined_file['path'],
			'extension' => $extension,
		);

		// Mark this cache version as processed.
		$this->set_done( $cache_ver );

		return $this->essb_cache_static_enqueue_files( $object, $status );
	}

	/**
	 * Build cache version key.
	 *
	 * Creates a unique cache key based on:
	 * - Plugin version
	 * - SSL status
	 * - Global cache version option
	 * - Individual script versions
	 *
	 * @since 2.0.0
	 *
	 * @param WP_Scripts|WP_Styles $object    WordPress scripts or styles object.
	 * @param array                $todo      Array of handles to process.
	 * @param string               $extension File extension.
	 * @return string MD5 hash of cache version.
	 */
	private function build_cache_version( $object, $todo, $extension ) {
		$ver = array();

		// Plugin version.
		$ver[] = 'essb_cache_static-ver-' . self::CACHE_VERSION;

		// SSL status affects URLs.
		$ver[] = 'is_ssl-' . (int) is_ssl();

		// Global cache version for manual purging.
		$ver[] = 'essb_cache_static_cache_ver-' . get_option( 'essb_cache_static_cache_ver', '1' );

		// Individual script versions.
		foreach ( $todo as $script ) {
			if ( isset( $object->registered[ $script ] ) ) {
				$registered_ver = $object->registered[ $script ]->ver;
				$ver[]          = sprintf( '%s-%s', $script, $registered_ver ? $registered_ver : 'no-ver' );
			}
		}

		return md5( 'essb_cache_static-' . implode( '-', $ver ) . '-' . $extension );
	}

	/**
	 * Process files for combination.
	 *
	 * Reads file contents and applies filters.
	 * Only processes ESSB-related files (starting with 'easy-social-' or 'essb').
	 *
	 * @since 2.0.0
	 *
	 * @param WP_Scripts|WP_Styles $object    WordPress scripts or styles object.
	 * @param array                $todo      Array of handles to process.
	 * @param string               $extension File extension.
	 * @return array Array of processed file contents keyed by handle.
	 */
	private function process_files( $object, $todo, $extension ) {
		$done = array();

		foreach ( $todo as $script ) {
			// Skip if not registered.
			if ( ! isset( $object->registered[ $script ] ) ) {
				continue;
			}

			$registered = $object->registered[ $script ];

			// Check 1: Handle name contains ESSB identifiers.
			$handle_match = ( false !== strpos( $script, 'easy-social-' ) || false !== strpos( $script, 'essb' ) );

			// Check 2: File URL contains ESSB3_PLUGIN_URL (if defined).
			$url_match = false;
			if ( defined( 'ESSB3_PLUGIN_URL' ) && ! empty( $registered->src ) ) {
				$url_match = ( false !== strpos( $registered->src, ESSB3_PLUGIN_URL ) );
			}

			// Only process if either handle OR URL matches ESSB.
			if ( ! $handle_match && ! $url_match ) {
				continue;
			}

			// Get relative path.
			$src = self::get_asset_relative_path( $object->base_url, $registered->src );

			// Handle pseudo-packages (like jquery) that have empty src.
			if ( empty( $registered->src ) || '' === $registered->src ) {
				$done[ $script ] = null;
				continue;
			}

			// Skip if file not local or doesn't exist.
			if ( ! $src || ! file_exists( ABSPATH . $src ) ) {
				continue;
			}

			// Read file content with error suppression.
			$file_path = ABSPATH . $src;
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
			$script_content = @file_get_contents( $file_path );

			if ( false === $script_content ) {
				continue;
			}

			// Apply filters to allow modification.
			$script_content = apply_filters(
				'essb_cache_static-item-' . $extension,
				$script_content,
				$object,
				$script
			);

			// If filter returns false, skip this file.
			if ( false === $script_content ) {
				continue;
			}

			// Minify CSS by removing extra whitespace.
			if ( 'css' === $extension ) {
				$script_content = $this->minify_css( $script_content );
			}

			$done[ $script ] = $script_content;
		}

		return $done;
	}

	/**
	 * Minify CSS content.
	 *
	 * Removes unnecessary whitespace while preserving functionality.
	 *
	 * @since 2.0.0
	 *
	 * @param string $css CSS content to minify.
	 * @return string Minified CSS.
	 */
	private function minify_css( $css ) {
		// Remove comments.
		$css = preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css );
		// Remove extra whitespace.
		$css = preg_replace( '/\s+/', ' ', $css );
		// Trim.
		$css = trim( $css );

		return $css;
	}

	/**
	 * Create combined file.
	 *
	 * Creates cache directory if needed and writes combined file.
	 * Files are stored in subfolders: css/ and js/
	 *
	 * @since 2.0.0
	 *
	 * @param array  $done      Array of file contents.
	 * @param string $cache_ver Cache version hash.
	 * @param string $extension File extension.
	 * @return array|false Array with 'path' and 'url' keys, or false on failure.
	 */
	private function create_combined_file( $done, $cache_ver, $extension ) {
		// Ensure base cache directory exists.
		if ( ! is_dir( $this->cache_base_dir ) ) {
			if ( ! wp_mkdir_p( $this->cache_base_dir ) ) {
				return false;
			}

			// Add .htaccess for security.
			$this->create_htaccess( $this->cache_base_dir );
		}

		// Create subdirectory for file type (css or js).
		$type_dir = $this->cache_base_dir . '/static/' . $extension;
		if ( ! is_dir( $type_dir ) ) {
			if ( ! wp_mkdir_p( $type_dir ) ) {
				return false;
			}
		}

		$combined_file_path = sprintf( '%s/%s.%s', $type_dir, $cache_ver, $extension );
		$combined_file_url  = sprintf( '%s/static/%s/%s.%s', $this->cache_base_url, $extension, $cache_ver, $extension );

		// Allow other plugins to modify the URL.
		$combined_file_url = apply_filters( 'essb_cache_static-url-' . $extension, $combined_file_url, $done );

		// Combine all content.
		$done_imploded = implode( "\n\n", $done );

		// Allow other plugins to minify and modify.
		$done_imploded = apply_filters( 'essb_cache_static-content-' . $extension, $done_imploded, $done );

		// Write file only if it doesn't exist.
		if ( ! file_exists( $combined_file_path ) ) {
			// Use WordPress filesystem API for better compatibility.
			global $wp_filesystem;

			if ( ! function_exists( 'WP_Filesystem' ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
			}

			WP_Filesystem();

			if ( ! $wp_filesystem || ! $wp_filesystem->put_contents( $combined_file_path, $done_imploded, FS_CHMOD_FILE ) ) {
				// Fallback to file_put_contents.
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
				if ( ! @file_put_contents( $combined_file_path, $done_imploded ) ) {
					return false;
				}
			}
		}

		return array(
			'path' => $combined_file_path,
			'url'  => $combined_file_url,
		);
	}

	/**
	 * Create .htaccess file in cache directory.
	 *
	 * Adds caching headers for optimal performance.
	 *
	 * @since 2.0.0
	 *
	 * @param string $cache_dir Cache directory path.
	 * @return void
	 */
	private function create_htaccess( $cache_dir ) {
		$htaccess_content = "# ESSB Static Cache
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType text/css \"access plus 1 year\"
ExpiresByType application/javascript \"access plus 1 year\"
</IfModule>

<IfModule mod_headers.c>
Header set Cache-Control \"public, max-age=31536000, immutable\"
</IfModule>
";

		$htaccess_file = $cache_dir . '/.htaccess';
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		@file_put_contents( $htaccess_file, $htaccess_content );
	}

	/**
	 * Enqueue combined files.
	 *
	 * Enqueues the combined file and preserves inline styles/scripts.
	 *
	 * @since 1.0.0
	 * @since 2.0.0 Improved inline content handling.
	 *
	 * @param WP_Scripts|WP_Styles $object WordPress scripts or styles object.
	 * @param array                $status Cache status array.
	 * @return array Modified todo array.
	 */
	public function essb_cache_static_enqueue_files( &$object, $status ) {
		$cache_ver = $status['cache_ver'];
		$todo      = $status['todo'];
		$done      = $status['done'];
		$url       = $status['url'];
		$extension = $status['extension'];

		switch ( $extension ) {
			case 'css':
				wp_enqueue_style( 'essb_cache_static-' . $cache_ver, $url, array(), null ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion

				// Preserve inline styles.
				foreach ( $done as $script ) {
					$inline_style = $object->get_data( $script, 'after' );
					if ( ! empty( $inline_style ) ) {
						if ( is_array( $inline_style ) ) {
							$inline_style = implode( "\n", $inline_style );
						}
						$object->add_inline_style( 'essb_cache_static-' . $cache_ver, $inline_style );
					}
				}
				break;

			case 'js':
				$in_footer = apply_filters( 'essb_cache_static-js-in-footer', true );
				wp_enqueue_script( 'essb_cache_static-' . $cache_ver, $url, array(), null, $in_footer ); // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion

				// Set correct group.
				$object->set_group( 'essb_cache_static-' . $cache_ver, false, $in_footer );

				// Preserve inline scripts (localization).
				$inline_data = array();
				foreach ( $done as $script ) {
					$data = $object->get_data( $script, 'data' );
					if ( ! empty( $data ) ) {
						$inline_data[] = $data;
					}
				}

				if ( ! empty( $inline_data ) ) {
					$object->add_data( 'essb_cache_static-' . $cache_ver, 'data', implode( "\n", $inline_data ) );
				}
				break;

			default:
				return $todo;
		}

		// Remove merged scripts from queue.
		$todo = array_diff( $todo, $done );

		// Add combined file to queue.
		$todo[] = 'essb_cache_static-' . $cache_ver;

		// Mark items as done.
		$object->done = array_merge( $object->done, $done );

		// Remove from processing queue.
		$object->queue = array_diff( $object->queue, $done );

		return $todo;
	}

	/**
	 * Mark a cache version as processed.
	 *
	 * @since 1.0.0
	 *
	 * @param string $handle Cache version handle.
	 * @return void
	 */
	public function set_done( $handle ) {
		$this->essb_cache_static_done[] = 'essb_cache_static-' . $handle;
	}

	/**
	 * Get array of processed cache versions.
	 *
	 * @since 1.0.0
	 *
	 * @return array Array of processed handles.
	 */
	public function get_done() {
		return $this->essb_cache_static_done;
	}

	/**
	 * Get relative path of an asset.
	 *
	 * Converts absolute URLs to relative paths for local file access.
	 *
	 * @since 1.0.0
	 * @since 2.0.0 Improved URL parsing.
	 *
	 * @param string $base_url Base URL of WordPress installation.
	 * @param string $item_url Full URL of the asset.
	 * @return string|false Relative path or false if not local.
	 */
	public static function get_asset_relative_path( $base_url, $item_url ) {
		// Remove protocol reference from base URL.
		$base_url = preg_replace( '/^(https?:\/\/|\/\/)/i', '', $base_url );

		// Check if this is a local asset.
		$src_parts = explode( $base_url, $item_url );

		// Get the trailing part of the URL.
		$maybe_relative = end( $src_parts );

		// Verify file exists.
		if ( ! file_exists( ABSPATH . $maybe_relative ) ) {
			return false;
		}

		return $maybe_relative;
	}

	/**
	 * Check if CSS cache folder exists.
	 *
	 * @since 2.0.0
	 *
	 * @return bool True if CSS cache folder exists.
	 */
	public function css_cache_folder_exists() {
		$css_dir = $this->cache_base_dir . '/css';
		return is_dir( $css_dir );
	}

	/**
	 * Check if JS cache folder exists.
	 *
	 * @since 2.0.0
	 *
	 * @return bool True if JS cache folder exists.
	 */
	public function js_cache_folder_exists() {
		$js_dir = $this->cache_base_dir . '/js';
		return is_dir( $js_dir );
	}

	/**
	 * Check if cache folder exists (any subfolder).
	 *
	 * @since 2.0.0
	 *
	 * @return bool True if any cache folder exists.
	 */
	public function cache_folder_exists() {
		return is_dir( $this->cache_base_dir );
	}

	/**
	 * Remove CSS cache folder completely.
	 *
	 * Deletes all CSS cache files and the CSS subfolder.
	 *
	 * @since 2.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function remove_css_cache_folder() {
		$css_dir = $this->cache_base_dir . '/css';
		
		if ( ! is_dir( $css_dir ) ) {
			return true; // Already removed.
		}

		return $this->remove_directory_recursive( $css_dir );
	}

	/**
	 * Remove JS cache folder completely.
	 *
	 * Deletes all JS cache files and the JS subfolder.
	 *
	 * @since 2.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function remove_js_cache_folder() {
		$js_dir = $this->cache_base_dir . '/js';
		
		if ( ! is_dir( $js_dir ) ) {
			return true; // Already removed.
		}

		return $this->remove_directory_recursive( $js_dir );
	}

	/**
	 * Remove entire cache folder structure.
	 *
	 * Deletes all cache files and folders (CSS and JS).
	 * Use with caution - this completely removes the cache directory.
	 *
	 * @since 2.0.0
	 *
	 * @return bool True on success, false on failure.
	 */
	public function remove_all_cache_folders() {
		if ( ! is_dir( $this->cache_base_dir ) ) {
			return true; // Already removed.
		}

		return $this->remove_directory_recursive( $this->cache_base_dir );
	}

	/**
	 * Recursively remove a directory and all its contents.
	 *
	 * @since 2.0.0
	 *
	 * @param string $dir Directory path to remove.
	 * @return bool True on success, false on failure.
	 */
	private function remove_directory_recursive( $dir ) {
		if ( ! is_dir( $dir ) ) {
			return false;
		}

		// Get all files and directories.
		$items = glob( $dir . '/*' );

		if ( false === $items ) {
			$items = array();
		}

		foreach ( $items as $item ) {
			if ( is_dir( $item ) ) {
				// Recursively remove subdirectory.
				$this->remove_directory_recursive( $item );
			} else {
				// Delete file.
				// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
				@unlink( $item );
			}
		}

		// Remove the directory itself.
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir
		return @rmdir( $dir );
	}

	/**
	 * Get cache base directory path.
	 *
	 * @since 2.0.0
	 *
	 * @return string Cache base directory path.
	 */
	public function get_cache_base_dir() {
		return $this->cache_base_dir;
	}

	/**
	 * Get cache base URL.
	 *
	 * @since 2.0.0
	 *
	 * @return string Cache base URL.
	 */
	public function get_cache_base_url() {
		return $this->cache_base_url;
	}

	/**
	 * Prevent cloning of singleton.
	 *
	 * @since 2.0.0
	 *
	 * @return void
	 */
	private function __clone() {
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Cloning is forbidden.', 'easy-social-share-buttons' ), '2.0.0' );
	}

	/**
	 * Prevent unserialization of singleton.
	 *
	 * @since 2.0.0
	 *
	 * @return void
	 */
	public function __wakeup() {
		_doing_it_wrong( __FUNCTION__, esc_html__( 'Unserializing is forbidden.', 'easy-social-share-buttons' ), '2.0.0' );
	}
}

/**
 * Backward compatibility class alias.
 *
 * Allows old code using ESSBStaticCache to continue working.
 *
 * @since 2.0.0
 */
class_alias( 'ESSB_Static_Cache', 'ESSBStaticCache' );

// Initialize the cache singleton.
$essb_cache_static_instance = ESSB_Static_Cache::instance();

// ============================================================================
// WordPress Filters for Content Processing
// ============================================================================

/**
 * Add filename comment to combined files.
 *
 * Prepends original filename as a comment for debugging.
 *
 * @since 1.0.0
 *
 * @param string               $content File content.
 * @param WP_Scripts|WP_Styles $object  WordPress object.
 * @param string               $script  Script handle.
 * @return string Modified content with comment.
 */
function essb_cache_static_comment_combined( $content, $object, $script ) {
	if ( ! $content ) {
		return $content;
	}

	return sprintf(
		"\n\n/* ESSBCacheStaticResources: %s */\n",
		$object->registered[ $script ]->src
	) . $content;
}
add_filter( 'essb_cache_static-item-css', 'essb_cache_static_comment_combined', 15, 3 );
add_filter( 'essb_cache_static-item-js', 'essb_cache_static_comment_combined', 15, 3 );

/**
 * Add table of contents to combined file.
 *
 * Lists all included files at the top of the combined file.
 *
 * @since 1.0.0
 *
 * @param string $content Combined content.
 * @param array  $items   Array of file contents.
 * @return string Content with TOC prepended.
 */
function essb_cache_static_add_toc( $content, $items ) {
	if ( ! $content || empty( $items ) ) {
		return $content;
	}

	$toc = array();

	foreach ( $items as $handle => $item_content ) {
		$toc[] = sprintf( ' - %s', $handle );
	}

	return sprintf( "/* TOC:\n%s\n*/", implode( "\n", $toc ) ) . $content;
}
add_filter( 'essb_cache_static-content-css', 'essb_cache_static_add_toc', 100, 2 );
add_filter( 'essb_cache_static-content-js', 'essb_cache_static_add_toc', 100, 2 );

/**
 * Resolve relative URLs in CSS to absolute URLs.
 *
 * Converts url() references to absolute paths.
 *
 * @since 1.0.0
 *
 * @param string    $content File content.
 * @param WP_Styles $object  WordPress styles object.
 * @param string    $script  Script handle.
 * @return string Modified content with absolute URLs.
 */
function essb_cache_static_resolve_css_urls( $content, $object, $script ) {
	if ( ! $content ) {
		return $content;
	}

	$src = ESSB_Static_Cache::get_asset_relative_path(
		$object->base_url,
		$object->registered[ $script ]->src
	);

	// Make all local asset URLs absolute.
	$content = preg_replace(
		'/url\(["\' ]?+(?!data:|https?:|\/\/)(.*?)["\' ]?\)/i',
		sprintf( "url('%s/$1')", $object->base_url . dirname( $src ) ),
		$content
	);

	return $content;
}
add_filter( 'essb_cache_static-item-css', 'essb_cache_static_resolve_css_urls', 10, 3 );

/**
 * Resolve CSS @import statements to absolute URLs.
 *
 * Converts relative @import paths to absolute URLs.
 *
 * @since 1.0.0
 *
 * @param string    $content File content.
 * @param WP_Styles $object  WordPress styles object.
 * @param string    $script  Script handle.
 * @return string Modified content with absolute import URLs.
 */
function essb_cache_static_resolve_css_imports( $content, $object, $script ) {
	if ( ! $content ) {
		return $content;
	}

	$src = ESSB_Static_Cache::get_asset_relative_path(
		$object->base_url,
		$object->registered[ $script ]->src
	);

	// Make all import URLs absolute.
	$content = preg_replace(
		'/@import\s+(url\()?["\'](?!https?:|\/\/)(.*?)["\'](\)?)/i',
		sprintf( "@import url('%s/$2')", $object->base_url . dirname( $src ) ),
		$content
	);

	return $content;
}
add_filter( 'essb_cache_static-item-css', 'essb_cache_static_resolve_css_imports', 10, 3 );

/**
 * Exclude CSS files with non-standard media queries.
 *
 * Only includes files with 'all', 'screen', or no media query.
 *
 * @since 1.0.0
 *
 * @param string    $content File content.
 * @param WP_Styles $object  WordPress styles object.
 * @param string    $script  Script handle.
 * @return string|false Content or false to exclude.
 */
function essb_cache_static_exclude_css_with_media_query( $content, $object, $script ) {
	if ( ! $content ) {
		return $content;
	}

	$whitelist = array( '', 'all', 'screen' );

	// Exclude from cache if non-standard media query.
	if ( ! in_array( $object->registered[ $script ]->args, $whitelist, true ) ) {
		return false;
	}

	return $content;
}
add_filter( 'essb_cache_static-item-css', 'essb_cache_static_exclude_css_with_media_query', 10, 3 );

/**
 * Ensure URLs use correct protocol (HTTP/HTTPS).
 *
 * Converts URLs to HTTPS when SSL is active.
 *
 * @since 1.0.0
 *
 * @param string $url File URL.
 * @return string URL with correct protocol.
 */
function essb_cache_static_maybe_ssl_url( $url ) {
	if ( is_ssl() ) {
		return str_replace( 'http://', 'https://', $url );
	}

	return $url;
}
add_filter( 'essb_cache_static-url-css', 'essb_cache_static_maybe_ssl_url' );
add_filter( 'essb_cache_static-url-js', 'essb_cache_static_maybe_ssl_url' );

// ============================================================================
// Cache Management Functions
// ============================================================================

/**
 * Purge static cache.
 *
 * Updates cache version to invalidate old files and triggers file deletion.
 * Note: Transients are no longer used to prevent database flooding.
 *
 * @since 1.0.0
 * @since 2.0.0 Removed transient caching, relies on file existence checks.
 *
 * @return void
 */
function purge_essb_cache_static_cache() {
	// Update global cache version to invalidate all caches.
	update_option( 'essb_cache_static_cache_ver', time() );

	// Add success notice if in admin.
	if ( is_admin() ) {
		add_action( 'admin_notices', 'essb_cache_static_cache_purged_success' );
	}

	// Allow other plugins to react to cache purge.
	do_action( 'essb_cache_static-cache-purge-delete' );
}

/**
 * Clear all ESSB cache transients from database.
 *
 * Note: Transients are no longer used in v2.0.0+ but this function
 * is kept for backward compatibility and to clean up old transients.
 *
 * @since 2.0.0
 * @deprecated 2.0.0 Transients no longer used to prevent database flooding.
 *
 * @return bool True if transients were deleted, false otherwise.
 */
function purge_essb_cache_static_transients() {
	global $wpdb;

	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
	$transients = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT option_name FROM {$wpdb->options} 
			WHERE option_name LIKE %s OR option_name LIKE %s",
			$wpdb->esc_like( '_transient_timeout_essb_cache_static-' ) . '%',
			$wpdb->esc_like( '_transient_essb_cache_static-' ) . '%'
		)
	);

	if ( empty( $transients ) ) {
		return false;
	}

	foreach ( $transients as $transient ) {
		// Extract transient name.
		$name = str_replace( array( '_transient_timeout_', '_transient_' ), '', $transient );
		delete_transient( $name );
	}

	return true;
}

/**
 * Display cache purge success notice.
 *
 * @since 1.0.0
 *
 * @return void
 */
function essb_cache_static_cache_purged_success() {
	printf(
		'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
		esc_html__( 'Success: ESSBCacheStaticResources cache purged.', 'easy-social-share-buttons' )
	);
}

/**
 * Delete all cached files from filesystem.
 *
 * Removes all .css and .js files from the cache directory.
 * Hooked to cache purge action for complete cleanup.
 *
 * @since 1.0.0
 * @since 2.0.0 Updated for new cache structure with subfolders.
 *
 * @return void
 */
function essb_cache_static_cache_delete_files() {
	$cache_instance = ESSB_Static_Cache::instance();
	$cache_base_dir = $cache_instance->get_cache_base_dir();

	// Delete CSS cache files.
	$css_dir = $cache_base_dir . '/css';
	if ( is_dir( $css_dir ) ) {
		$css_files = glob( $css_dir . '/*.css' );
		if ( is_array( $css_files ) ) {
			foreach ( $css_files as $css_file ) {
				if ( is_file( $css_file ) ) {
					// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
					@unlink( $css_file );
				}
			}
		}
	}

	// Delete JS cache files.
	$js_dir = $cache_base_dir . '/js';
	if ( is_dir( $js_dir ) ) {
		$js_files = glob( $js_dir . '/*.js' );
		if ( is_array( $js_files ) ) {
			foreach ( $js_files as $js_file ) {
				if ( is_file( $js_file ) ) {
					// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
					@unlink( $js_file );
				}
			}
		}
	}

	// Also clean up legacy location if exists.
	$upload_dir          = wp_upload_dir();
	$legacy_cache_dir    = $upload_dir['basedir'] . '/essb_cache_static';
	$legacy_cache_files  = glob( $legacy_cache_dir . '/*' );
	
	if ( is_array( $legacy_cache_files ) ) {
		foreach ( $legacy_cache_files as $legacy_file ) {
			if ( is_file( $legacy_file ) && '.htaccess' !== basename( $legacy_file ) ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.unlink_unlink
				@unlink( $legacy_file );
			}
		}
	}
}
add_action( 'essb_cache_static-cache-purge-delete', 'essb_cache_static_cache_delete_files' );

// ============================================================================
// Public Helper Functions for Cache Folder Management
// ============================================================================

/**
 * Check if CSS cache folder exists.
 *
 * @since 2.0.0
 *
 * @return bool True if CSS cache folder exists.
 */
function essb_cache_css_folder_exists() {
	$cache_instance = ESSB_Static_Cache::instance();
	return $cache_instance->css_cache_folder_exists();
}

/**
 * Check if JS cache folder exists.
 *
 * @since 2.0.0
 *
 * @return bool True if JS cache folder exists.
 */
function essb_cache_js_folder_exists() {
	$cache_instance = ESSB_Static_Cache::instance();
	return $cache_instance->js_cache_folder_exists();
}

/**
 * Check if any cache folder exists.
 *
 * @since 2.0.0
 *
 * @return bool True if cache folder exists.
 */
function essb_cache_folder_exists() {
	$cache_instance = ESSB_Static_Cache::instance();
	return $cache_instance->cache_folder_exists();
}

/**
 * Remove CSS cache folder completely.
 *
 * Use this when CSS caching is disabled to clean up.
 *
 * @since 2.0.0
 *
 * @return bool True on success, false on failure.
 */
function essb_remove_css_cache_folder() {
	$cache_instance = ESSB_Static_Cache::instance();
	
	// Remove the folder.
	return $cache_instance->remove_css_cache_folder();
}

/**
 * Remove JS cache folder completely.
 *
 * Use this when JS caching is disabled to clean up.
 *
 * @since 2.0.0
 *
 * @return bool True on success, false on failure.
 */
function essb_remove_js_cache_folder() {
	$cache_instance = ESSB_Static_Cache::instance();
	
	// Remove the folder.
	return $cache_instance->remove_js_cache_folder();
}

/**
 * Remove all cache folders completely.
 *
 * Use this when all caching is disabled or for complete cleanup.
 * WARNING: This removes the entire cache directory structure.
 *
 * @since 2.0.0
 *
 * @return bool True on success, false on failure.
 */
function essb_remove_all_cache_folders() {
	$cache_instance = ESSB_Static_Cache::instance();
	
	// Remove all folders.
	return $cache_instance->remove_all_cache_folders();
}

/**
 * Get cache folder information.
 *
 * Returns an array with cache folder details for debugging/display.
 *
 * @since 2.0.0
 *
 * @return array {
 *     Cache folder information.
 *
 *     @type string $base_dir     Base cache directory path.
 *     @type string $base_url     Base cache URL.
 *     @type bool   $exists       Whether base directory exists.
 *     @type bool   $css_exists   Whether CSS subfolder exists.
 *     @type bool   $js_exists    Whether JS subfolder exists.
 *     @type int    $css_files    Number of CSS cache files.
 *     @type int    $js_files     Number of JS cache files.
 *     @type string $size         Total cache size (formatted).
 * }
 */
function essb_get_cache_folder_info() {
	$cache_instance = ESSB_Static_Cache::instance();
	$base_dir       = $cache_instance->get_cache_base_dir();
	$base_url       = $cache_instance->get_cache_base_url();

	$info = array(
		'base_dir'   => $base_dir,
		'base_url'   => $base_url,
		'exists'     => is_dir( $base_dir ),
		'css_exists' => $cache_instance->css_cache_folder_exists(),
		'js_exists'  => $cache_instance->js_cache_folder_exists(),
		'css_files'  => 0,
		'js_files'   => 0,
		'size'       => '0 B',
	);

	// Count CSS files.
	if ( $info['css_exists'] ) {
		$css_files = glob( $base_dir . '/static/css/*.css' );
		if ( is_array( $css_files ) ) {
			$info['css_files'] = count( $css_files );
		}
	}

	// Count JS files.
	if ( $info['js_exists'] ) {
		$js_files = glob( $base_dir . '/static/js/*.js' );
		if ( is_array( $js_files ) ) {
			$info['js_files'] = count( $js_files );
		}
	}

	// Calculate total size.
	if ( $info['exists'] ) {
		$total_size = 0;
		$all_files  = glob( $base_dir . '/static/{css,js}/*.{css,js}', GLOB_BRACE );
		
		if ( is_array( $all_files ) ) {
			foreach ( $all_files as $file ) {
				if ( is_file( $file ) ) {
					$total_size += filesize( $file );
				}
			}
		}
		
		$info['size'] = size_format( $total_size, 2 );
	}

	return $info;
}