<?php
/**
 * BaseCloud Boost — Cache Purge Handler
 *
 * @package BaseCloud\Boost\Core
 */

declare( strict_types=1 );

namespace BaseCloud\Boost\Core;

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

/**
 * Handles all cache invalidation logic with smart, targeted purging.
 */
class Cache_Purge {

	/** @var Page_Cache */
	private Page_Cache $page_cache;

	/**
	 * Constructor.
	 */
	public function __construct() {
		$this->page_cache = new Page_Cache();
	}

	/**
	 * Purge ALL cached files site-wide: disk page cache AND the minified-asset
	 * cache. Minified/combined bundles are part of "the cache" — before this,
	 * a stale or corrupt bundle survived every purge for up to two weeks (the
	 * weekly cleanup cron was the only thing that ever removed it).
	 *
	 * @return array{pages:int, minified:int} Counts of what was actually purged
	 *                                        (for honest purge feedback).
	 */
	public function purge_all(): array {
		$summary = [ 'pages' => 0, 'minified' => 0 ];

		// Snapshot the page count before deletion so feedback can report it.
		try {
			$summary['pages'] = (int) ( $this->page_cache->get_cache_stats()['count'] ?? 0 );
		} catch ( \Throwable $e ) {
			// Stats are cosmetic — never let them block a purge.
		}

		$this->page_cache->delete_all_cache();
		$summary['minified'] = $this->delete_minify_cache();

		// Regenerate the options snapshot advanced-cache.php reads on every
		// request. delete_all_cache() preserves it, but if it was missing or
		// removed by anything else the purge must leave the drop-in functional.
		if ( class_exists( \BaseCloud\Boost\Admin\Settings_API::class ) ) {
			try {
				\BaseCloud\Boost\Admin\Settings_API::write_options_cache();
			} catch ( \Throwable $e ) {
				if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
					error_log( '[BaseCloud Boost] Could not regenerate bcboost-options.json after purge: ' . $e->getMessage() );
				}
			}
		}

		do_action( 'BCBOOST_after_purge_all' );

		// Re-warm the whole cache without waiting for visitor traffic. This
		// resets any run already in flight (fresh run_id kills old chain links)
		// — a second purge mid-warm restarts cleanly instead of double-chaining.
		// (Also fixes the old cleanup here, which called delete_transient() on
		// the queue even though it is stored as an option, leaving it stale.)
		$this->maybe_start_preload();

		return $summary;
	}

	/**
	 * Kick the preloader after a full purge, when 'Auto-Preload After Cache
	 * Clear' is enabled. Never fatal, never runs twice in one request, and
	 * Cache_Preloader itself skips (with a log line) on hosts where Boost's
	 * page cache is inactive (WP Engine, Kinsta…).
	 *
	 * @return void
	 */
	private function maybe_start_preload(): void {
		static $fired = false;
		if ( $fired ) {
			return;
		}
		$fired = true;

		try {
			$preloader = new Cache_Preloader();

			if ( ! BCBOOST_get_option( 'preload_on_purge', true ) ) {
				// Auto-preload off: still clear any stale run so status stays honest.
				if ( $preloader->is_running() ) {
					$preloader->stop_preload();
				}
				return;
			}

			$preloader->start_preload();
		} catch ( \Throwable $e ) {
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( '[BaseCloud Boost] Could not start preloader after purge: ' . $e->getMessage() );
			}
		}
	}

	/**
	 * Empty the minified-asset cache (BCBOOST_MINIFY_DIR).
	 *
	 * Preserved, mirroring the weekly sweep's rules
	 * (Cache_Engine::cleanup_minify_cache()):
	 * - the `fonts/` subtree — self-hosted font CSS/woff2 are long-lived and
	 *   referenced by pages other layers (CDN edge, browsers) may still serve;
	 *   deleting them would 404 fonts until every downstream cache refills.
	 * - `index.php` directory-listing guards and `bcboost-options.json` (if it
	 *   ever lives in this tree).
	 *
	 * Bundles regenerate automatically on the next uncached page view (the
	 * minifiers write content-hashed filenames on demand), so this is always
	 * safe — worst case is one slower first view.
	 *
	 * @return int Number of files deleted.
	 */
	private function delete_minify_cache(): int {
		if ( ! defined( 'BCBOOST_MINIFY_DIR' ) || ! is_dir( BCBOOST_MINIFY_DIR ) ) {
			return 0;
		}

		$deleted  = 0;
		$preserve = [ 'index.php', 'bcboost-options.json' ];

		try {
			$iterator = new \RecursiveIteratorIterator(
				new \RecursiveDirectoryIterator( BCBOOST_MINIFY_DIR, \FilesystemIterator::SKIP_DOTS ),
				\RecursiveIteratorIterator::CHILD_FIRST
			);

			foreach ( $iterator as $item ) {
				/** @var \SplFileInfo $item */
				$normalized = str_replace( '\\', '/', $item->getPathname() );

				// Keep the whole fonts/ subtree (directory itself included).
				if ( preg_match( '#/fonts(/|$)#', $normalized ) ) {
					continue;
				}

				if ( $item->isDir() ) {
					// Fails harmlessly when the directory still holds preserved
					// files; it is recreated on demand by wp_mkdir_p() anyway.
					@rmdir( $item->getPathname() );
					continue;
				}

				if ( in_array( $item->getFilename(), $preserve, true ) ) {
					continue;
				}

				if ( @unlink( $item->getPathname() ) ) {
					$deleted++;
				}
			}
		} catch ( \Throwable $e ) {
			// Fail open: a cache wipe must never take a page (or wp-admin) down.
			if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
				error_log( '[BaseCloud Boost] Minify-cache purge failed: ' . $e->getMessage() );
			}
		}

		return $deleted;
	}

	/**
	 * Purge cache for a specific URL.
	 *
	 * @param string $url Full URL to purge.
	 * @return void
	 */
	public function purge_url( string $url ): void {
		do_action( 'BCBOOST_before_purge_url', $url );
		$this->page_cache->delete_cache_file( $url );
		do_action( 'BCBOOST_after_purge_url', $url );
	}

	/**
	 * Smart-purge for a post: removes the post URL, homepage, archives, and taxonomy pages.
	 *
	 * @param int $post_id Post ID.
	 * @return void
	 */
	public function purge_post( int $post_id ): void {
		$post = get_post( $post_id );
		if ( ! $post ) {
			return;
		}

		$urls_to_purge = [];

		// The post itself.
		$permalink = get_permalink( $post_id );
		if ( $permalink ) {
			$urls_to_purge[] = $permalink;
		}

		// Homepage.
		$urls_to_purge[] = home_url( '/' );

		// Blog index (if different from homepage).
		$page_for_posts = get_option( 'page_for_posts' );
		if ( $page_for_posts ) {
			$urls_to_purge[] = get_permalink( (int) $page_for_posts );
		}

		// Post type archive.
		$post_type_archive = get_post_type_archive_link( $post->post_type );
		if ( $post_type_archive ) {
			$urls_to_purge[] = $post_type_archive;
		}

		// Category & tag archives.
		$taxonomies = get_object_taxonomies( $post->post_type, 'objects' );
		foreach ( $taxonomies as $taxonomy ) {
			$terms = wp_get_post_terms( $post_id, $taxonomy->name );
			if ( is_array( $terms ) ) {
				foreach ( $terms as $term ) {
					$term_link = get_term_link( $term );
					if ( ! is_wp_error( $term_link ) ) {
						$urls_to_purge[] = $term_link;
					}
				}
			}
		}

		// Author archive.
		$author_link = get_author_posts_url( (int) $post->post_author );
		if ( $author_link ) {
			$urls_to_purge[] = $author_link;
		}

		// Paginated variants of the post.
		$page_count = substr_count( $post->post_content, '<!--nextpage-->' );
		if ( $page_count > 0 ) {
			for ( $i = 2; $i <= ( $page_count + 1 ); $i++ ) {
				$urls_to_purge[] = trailingslashit( $permalink ) . user_trailingslashit( $i, 'single_paged' );
			}
		}

		foreach ( array_unique( array_filter( $urls_to_purge ) ) as $url ) {
			$this->page_cache->delete_cache_file( $url );
		}

		do_action( 'BCBOOST_after_purge_post', $post_id, $urls_to_purge );

		// Immediately re-warm the post itself + the front page (fire-and-forget)
		// so the very next visitor gets a cache HIT instead of paying the MISS.
		// Publish-status only: never warm trashed/private URLs. warm_urls_async()
		// dedupes per request and no-ops where Boost's page cache is inactive.
		if ( $post->post_status === 'publish' && BCBOOST_get_option( 'cache_warm_on_purge', true ) ) {
			try {
				( new Cache_Preloader() )->warm_urls_async( [
					(string) $permalink,
					home_url( '/' ),
				] );
			} catch ( \Throwable $e ) {
				if ( defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
					error_log( '[BaseCloud Boost] Post re-warm failed: ' . $e->getMessage() );
				}
			}
		}
	}

	/**
	 * Purge a taxonomy term archive.
	 *
	 * @param int    $term_id  Term ID.
	 * @param string $taxonomy Taxonomy slug.
	 * @return void
	 */
	public function purge_taxonomy_term( int $term_id, string $taxonomy ): void {
		$term_link = get_term_link( $term_id, $taxonomy );
		if ( ! is_wp_error( $term_link ) ) {
			$this->page_cache->delete_cache_file( $term_link );
		}
		$this->page_cache->delete_cache_file( home_url( '/' ) );

		do_action( 'BCBOOST_after_purge_term', $term_id, $taxonomy );
	}

	/**
	 * Purge an author's archive page.
	 *
	 * @param int $user_id WordPress user ID.
	 * @return void
	 */
	public function purge_author( int $user_id ): void {
		$author_url = get_author_posts_url( $user_id );
		if ( $author_url ) {
			$this->page_cache->delete_cache_file( $author_url );
		}
	}

	/**
	 * Purge cached files whose path contains a given tag/pattern.
	 *
	 * @param string $tag Partial URL string to match.
	 * @return int Number of files purged.
	 */
	public function purge_by_tag( string $tag ): int {
		$count    = 0;
		$all_urls = $this->page_cache->get_cached_urls();

		foreach ( $all_urls as $url ) {
			if ( strpos( $url, $tag ) !== false ) {
				$this->purge_url( $url );
				$count++;
			}
		}

		return $count;
	}
}
