<?php
/**
 * Cache module — Storage manager
 *
 * Single source of truth for filesystem paths used by the cache module.
 * Other components must NOT compute paths themselves.
 *
 * Layout (per blog):
 *   uploads/blaminhor-essentials/cache/
 *     pages/{scheme}/{host}/{path}/index[-variant].html(.gz)
 *     critical/templates/{template}-{viewport}.css
 *     critical/overrides/{url-hash}.css
 *     critical/manifest.json
 *     min/css/{hash}.css(.gz)
 *     min/js/{hash}.js(.gz)
 *     min/manifest.json
 *     logs/*.log
 *     .safe_mode               (kill-switch flag, read by drop-in)
 *     .dropin_version          (version of installed advanced-cache.php)
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Storage {

	/**
	 * Multisite helper used to scope paths per blog.
	 *
	 * @var Blaminhor_Essentials_Cache_Multisite
	 */
	private $multisite;

	public function __construct( Blaminhor_Essentials_Cache_Multisite $multisite ) {
		$this->multisite = $multisite;
	}

	/**
	 * Ensure all base directories exist (and are protected).
	 *
	 * @return bool True if all directories exist after the call.
	 */
	public function ensure_directories() {
		$dirs = array(
			$this->base(),
			$this->pages_dir(),
			$this->critical_dir(),
			$this->critical_dir() . 'templates/',
			$this->critical_dir() . 'overrides/',
			$this->min_dir(),
			$this->min_dir() . 'css/',
			$this->min_dir() . 'js/',
			$this->logs_dir(),
		);

		$ok = true;
		foreach ( $dirs as $dir ) {
			if ( ! blaminhor_essentials_ensure_protected_directory( untrailingslashit( $dir ) ) ) {
				$ok = false;
			}
		}
		return $ok;
	}

	/**
	 * Cache root path (per blog).
	 *
	 * @return string Trailing-slashed.
	 */
	public function base() {
		return $this->multisite->get_cache_dir();
	}

	public function base_url() {
		return $this->multisite->get_cache_url();
	}

	/**
	 * Page-cache HTML output dir.
	 *
	 * @return string
	 */
	public function pages_dir() {
		return $this->multisite->get_cache_dir( 'pages' );
	}

	/**
	 * Compute the directory for a specific URL's cached HTML, segmented by
	 * variant.
	 *
	 * Layout: `{pages_dir}/{scheme}/{host}/{path-with-trailing-slash}`
	 * Filename inside that dir is the variant key (see Page_Cache::variant_key()).
	 *
	 * @param string $url Absolute URL.
	 * @return string Trailing-slashed absolute path.
	 */
	public function url_to_cache_dir( $url ) {
		$parts = wp_parse_url( $url );
		$scheme = isset( $parts['scheme'] ) ? $parts['scheme'] : 'https';
		$host   = isset( $parts['host'] ) ? strtolower( $parts['host'] ) : '';
		$path   = isset( $parts['path'] ) ? $parts['path'] : '/';

		// Sanitise host (only alphanum, dot, hyphen, underscore).
		$host = preg_replace( '#[^a-z0-9._-]#i', '_', $host );

		// Normalise path: lower, replace risky chars, ensure trailing slash, no '..'.
		$path = '/' . trim( $path, '/' ) . '/';
		$path = str_replace( '..', '_', $path );
		$path = preg_replace( '#[^a-zA-Z0-9._/-]#', '_', $path );

		return trailingslashit( $this->pages_dir() ) . $scheme . '/' . $host . $path;
	}

	/**
	 * Critical CSS dir.
	 *
	 * @return string
	 */
	public function critical_dir() {
		return $this->multisite->get_cache_dir( 'critical' );
	}

	/**
	 * Minified bundles dir.
	 *
	 * @return string
	 */
	public function min_dir() {
		return $this->multisite->get_cache_dir( 'min' );
	}

	public function min_url() {
		return $this->multisite->get_cache_url( 'min' );
	}

	/**
	 * Logs dir.
	 *
	 * @return string
	 */
	public function logs_dir() {
		return $this->multisite->get_cache_dir( 'logs' );
	}

	/**
	 * Path to the safe-mode flag. The drop-in checks this VERY early.
	 *
	 * Note: this is read by advanced-cache.php BEFORE WordPress loads. We pre-
	 * compute the absolute path at install time and bake it into the drop-in,
	 * but the same value must be readable here so the admin UI can toggle it.
	 *
	 * @return string
	 */
	public function safe_mode_flag_path() {
		return $this->base() . '.safe_mode';
	}

	/**
	 * Path to the drop-in version marker (used to detect drop-in tampering).
	 *
	 * @return string
	 */
	public function dropin_version_path() {
		return $this->base() . '.dropin_version';
	}

	/**
	 * Disk usage for the entire cache tree (in bytes).
	 *
	 * @return int
	 */
	public function disk_usage() {
		$dir = untrailingslashit( $this->base() );
		if ( ! is_dir( $dir ) ) {
			return 0;
		}
		$size = 0;
		try {
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $dir, FilesystemIterator::SKIP_DOTS ),
				RecursiveIteratorIterator::CHILD_FIRST
			);
			foreach ( $iterator as $file ) {
				if ( $file->isFile() ) {
					$size += $file->getSize();
				}
			}
		} catch ( Exception $e ) {
			blaminhor_essentials_log( 'Cache disk_usage error: ' . $e->getMessage(), 'error' );
		}
		return (int) $size;
	}

	/**
	 * Hash an arbitrary URL for use as a filename or DB key.
	 *
	 * @param string $url Absolute URL.
	 * @return string sha1 of the canonical URL (lowercased host, trailing slash normalised).
	 */
	public function hash_url( $url ) {
		$parts = wp_parse_url( $url );
		if ( ! is_array( $parts ) ) {
			return sha1( $url );
		}
		$host = isset( $parts['host'] ) ? strtolower( $parts['host'] ) : '';
		$path = isset( $parts['path'] ) ? $parts['path'] : '/';
		$path = '/' . ltrim( $path, '/' );
		return sha1( $host . $path );
	}

	/**
	 * Recursively delete an absolute path inside our base tree. Safety: refuses
	 * to operate outside of $this->base() to prevent accidents.
	 *
	 * @param string $path Absolute path.
	 * @return bool
	 */
	public function rrmdir( $path ) {
		$base = realpath( untrailingslashit( $this->base() ) );
		$real = realpath( $path );
		if ( false === $base || false === $real ) {
			return false;
		}
		// The path must be inside our base tree.
		if ( 0 !== strpos( $real, $base ) ) {
			return false;
		}
		if ( is_file( $real ) ) {
			return @unlink( $real );
		}
		if ( ! is_dir( $real ) ) {
			return true;
		}
		$ok = true;
		try {
			$iterator = new RecursiveIteratorIterator(
				new RecursiveDirectoryIterator( $real, FilesystemIterator::SKIP_DOTS ),
				RecursiveIteratorIterator::CHILD_FIRST
			);
			foreach ( $iterator as $f ) {
				if ( $f->isDir() ) {
					if ( ! @rmdir( $f->getPathname() ) ) {
						$ok = false;
					}
				} elseif ( ! @unlink( $f->getPathname() ) ) {
					$ok = false;
				}
			}
			if ( ! @rmdir( $real ) ) {
				$ok = false;
			}
		} catch ( Exception $e ) {
			blaminhor_essentials_log( 'Cache rrmdir error: ' . $e->getMessage(), 'error' );
			$ok = false;
		}
		return $ok;
	}
}
