<?php
/**
 * Cache module — .htaccess writer
 *
 * Idempotent helper to insert and remove blocks of directives within the
 * site's .htaccess. Each block is delimited by markers so we never touch
 * directives we did not create:
 *
 *   # BEGIN Blaminhor Essentials Cache: <block-id>
 *   ...directives...
 *   # END Blaminhor Essentials Cache: <block-id>
 *
 * Reuses WordPress core's `insert_with_markers()` (it does the right thing
 * for backups and atomic writes).
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Htaccess_Writer {

	const MARKER_PREFIX = 'Blaminhor Essentials Cache: ';

	/**
	 * @return string Path to the site's .htaccess (may not exist).
	 */
	public function get_htaccess_path() {
		return ABSPATH . '.htaccess';
	}

	/**
	 * @return bool Whether the file exists and is writable (or can be created).
	 */
	public function is_writable() {
		$path = $this->get_htaccess_path();
		if ( file_exists( $path ) ) {
			return is_writable( $path );
		}
		// Can we create it?
		return is_writable( ABSPATH );
	}

	/**
	 * Insert or replace a marked block of directives.
	 *
	 * @param string   $block_id Stable identifier (e.g. 'browser-cache', 'gzip').
	 * @param string[] $lines    Directives, one per line. Pass an empty array to remove.
	 * @return bool
	 */
	public function write_block( $block_id, $lines ) {
		$path = $this->get_htaccess_path();
		if ( ! $this->is_writable() ) {
			return false;
		}
		// Initialise WP_Filesystem to delegate atomic IO + permissions handling.
		if ( ! function_exists( 'insert_with_markers' ) ) {
			require_once ABSPATH . 'wp-admin/includes/misc.php';
		}
		// Touch the file so insert_with_markers has something to read on first run.
		if ( ! file_exists( $path ) ) {
			@touch( $path );
		}
		return (bool) insert_with_markers( $path, self::MARKER_PREFIX . $block_id, (array) $lines );
	}

	/**
	 * Remove a previously-written block. No-op if absent.
	 *
	 * insert_with_markers() with an empty $insertion still writes the BEGIN/END
	 * markers and the auto-generated WP comment header, so we strip the block
	 * (markers + body) directly from the file. Falls back gracefully if the
	 * file is missing or unreadable.
	 *
	 * @param string $block_id
	 * @return bool
	 */
	public function remove_block( $block_id ) {
		$path = $this->get_htaccess_path();
		if ( ! file_exists( $path ) ) {
			return true;
		}
		if ( ! is_readable( $path ) || ! is_writable( $path ) ) {
			return false;
		}
		$contents = @file_get_contents( $path );
		if ( false === $contents ) {
			return false;
		}
		$marker  = preg_quote( self::MARKER_PREFIX . $block_id, '/' );
		$pattern = '/(?:\r\n|\r|\n)?[ \t]*#[ \t]*BEGIN[ \t]+' . $marker . '[ \t]*.*?#[ \t]*END[ \t]+' . $marker . '[ \t]*(?:\r\n|\r|\n)?/s';
		$updated = preg_replace( $pattern, "\n", $contents );
		if ( null === $updated ) {
			return false;
		}
		if ( $updated === $contents ) {
			return true; // No matching block, nothing to do.
		}
		// Collapse runs of blank lines created by the removal.
		$updated = preg_replace( "/(?:\r\n|\r|\n){3,}/", "\n\n", $updated );
		// Strip leading blank lines.
		$updated = ltrim( $updated, "\r\n" );
		$bytes   = @file_put_contents( $path, $updated, LOCK_EX );
		return false !== $bytes;
	}

	/**
	 * Remove ALL blocks created by this module. Useful at deactivation.
	 *
	 * @param string[] $block_ids List of known block IDs to clean up.
	 * @return bool True if every removal succeeded.
	 */
	public function remove_all( $block_ids ) {
		$ok = true;
		foreach ( (array) $block_ids as $id ) {
			if ( ! $this->remove_block( $id ) ) {
				$ok = false;
			}
		}
		return $ok;
	}

	/**
	 * Whether a block with the given ID currently exists in the file.
	 *
	 * @param string $block_id
	 * @return bool
	 */
	public function block_exists( $block_id ) {
		$path = $this->get_htaccess_path();
		if ( ! file_exists( $path ) ) {
			return false;
		}
		$contents = @file_get_contents( $path );
		if ( false === $contents ) {
			return false;
		}
		$marker = self::MARKER_PREFIX . $block_id;
		return false !== strpos( $contents, '# BEGIN ' . $marker );
	}
}
