<?php
/**
 * Cache module — Purge API + invalidation hooks
 *
 * Public API:
 *   purge_all()          — wipe everything under pages/
 *   purge_url($url)      — wipe all variants for one URL
 *   purge_post($post_id) — permalink + home + author archive + post-type archive + terms
 *   purge_term($term_id) — term archive + paginated archives
 *
 * Hooks: registered to fire automatically on save_post / comment changes /
 * theme/customizer changes / nav menu updates / WC stock updates.
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Purge {

	/** @var Blaminhor_Essentials_Cache_Storage */
	private $storage;

	public function __construct( Blaminhor_Essentials_Cache_Storage $storage ) {
		$this->storage = $storage;
	}

	public function register_hooks() {
		// Posts.
		add_action( 'save_post', array( $this, 'on_save_post' ), 10, 2 );
		add_action( 'deleted_post', array( $this, 'on_deleted_post' ) );
		add_action( 'wp_trash_post', array( $this, 'on_deleted_post' ) );
		add_action( 'untrashed_post', array( $this, 'on_deleted_post' ) );
		add_action( 'transition_post_status', array( $this, 'on_transition_post_status' ), 10, 3 );
		add_action( 'clean_post_cache', array( $this, 'on_deleted_post' ) );

		// Comments.
		add_action( 'comment_post', array( $this, 'on_comment_change' ) );
		add_action( 'edit_comment', array( $this, 'on_comment_change' ) );
		add_action( 'deleted_comment', array( $this, 'on_comment_change' ) );
		add_action( 'wp_set_comment_status', array( $this, 'on_comment_change' ) );

		// Terms.
		add_action( 'created_term', array( $this, 'on_term_change' ), 10, 3 );
		add_action( 'edited_term', array( $this, 'on_term_change' ), 10, 3 );
		add_action( 'delete_term', array( $this, 'on_term_change' ), 10, 3 );

		// Theme & customizer.
		add_action( 'switch_theme', array( $this, 'purge_all' ) );
		add_action( 'customize_save_after', array( $this, 'purge_all' ) );

		// Menus & widgets.
		add_action( 'wp_update_nav_menu', array( $this, 'purge_all' ) );
		add_action( 'update_option_sidebars_widgets', array( $this, 'purge_all' ) );

		// Permalinks / site identity changes.
		foreach ( array( 'permalink_structure', 'blogname', 'blogdescription', 'home', 'siteurl' ) as $opt ) {
			add_action( 'update_option_' . $opt, array( $this, 'purge_all' ) );
		}

		// User profile changes (impact author archives).
		add_action( 'profile_update', array( $this, 'on_user_change' ) );
		add_action( 'user_register', array( $this, 'on_user_change' ) );
		add_action( 'delete_user', array( $this, 'on_user_change' ) );

		// WooCommerce-aware (no-op if WC absent).
		add_action( 'woocommerce_product_set_stock', array( $this, 'on_wc_product_change' ) );
		add_action( 'woocommerce_variation_set_stock', array( $this, 'on_wc_product_change' ) );

		// Cross-module purge request — any plugin module can fire this action
		// when its own state changes invalidate cached frontend output (e.g.
		// redirections rules, maintenance mode toggle, etc.).
		add_action( 'blaminhor_essentials_cache_purge_request', array( $this, 'purge_all' ) );

		// Cross-module per-post purge — fired by Broken Links when it discovers
		// a dead link inside a specific post (the cached HTML for that post now
		// shows a link we know is broken). Cheaper than a full site purge.
		add_action( 'blaminhor_essentials_cache_purge_post_request', array( $this, 'on_cross_module_post_purge' ), 10, 1 );
	}

	/**
	 * Adapter for blaminhor_essentials_cache_purge_post_request.
	 *
	 * @param int $post_id
	 */
	public function on_cross_module_post_purge( $post_id ) {
		$post_id = (int) $post_id;
		if ( $post_id <= 0 ) {
			return;
		}
		$post = get_post( $post_id );
		if ( ! $post ) {
			return;
		}
		$this->purge_post( $post );
	}

	/* ------------------------------------------------------------------ */
	/*  Public API                                                         */
	/* ------------------------------------------------------------------ */

	/**
	 * Wipe the entire pages/ directory. Cheapest robust purge.
	 *
	 * @return bool
	 */
	public function purge_all() {
		$pages = $this->storage->pages_dir();
		$ok    = $this->storage->rrmdir( $pages );
		// Always recreate the empty dir + protective files so future writes succeed.
		blaminhor_essentials_ensure_protected_directory( untrailingslashit( $pages ) );
		update_option( 'blaminhor_essentials_cache_last_purge', time() );
		if ( class_exists( 'Blaminhor_Essentials_Cache_Pages_Index' ) ) {
			Blaminhor_Essentials_Cache_Pages_Index::instance()->record_purge_all();
		}
		do_action( 'blaminhor_essentials_cache_purged_all' );
		return (bool) $ok;
	}

	/**
	 * Purge all variants for a single URL.
	 *
	 * @param string $url Absolute URL.
	 * @return int Number of files deleted.
	 */
	public function purge_url( $url ) {
		if ( empty( $url ) ) {
			return 0;
		}
		$dir = $this->storage->url_to_cache_dir( $url );
		if ( ! is_dir( $dir ) ) {
			return 0;
		}
		$count = 0;
		foreach ( (array) glob( $dir . '*.html*' ) as $f ) {
			if ( @unlink( $f ) ) {
				$count++;
			}
		}
		// Also wipe redirect cache files for this URL — they are tracked by the
		// pages index alongside HTML, so the visible count would otherwise lag
		// behind the actual disk state after a redirect was purged.
		foreach ( (array) glob( $dir . '*.redirect' ) as $f ) {
			if ( @unlink( $f ) ) {
				$count++;
			}
		}
		if ( class_exists( 'Blaminhor_Essentials_Cache_Pages_Index' ) ) {
			Blaminhor_Essentials_Cache_Pages_Index::instance()->record_purge( $url );
		}
		do_action( 'blaminhor_essentials_cache_purged_url', $url, $count );
		return $count;
	}

	/**
	 * Purge a post + the URLs that listed it (home, archives, taxonomies).
	 *
	 * @param int $post_id
	 * @return void
	 */
	public function purge_post( $post_id ) {
		$post = get_post( $post_id );
		if ( ! $post ) {
			return;
		}
		// Skip auto-draft / revision / nav menu items / etc.
		if ( in_array( $post->post_type, array( 'revision', 'auto-draft', 'nav_menu_item', 'customize_changeset' ), true ) ) {
			return;
		}

		$urls = $this->collect_urls_for_post( $post );
		foreach ( $urls as $url ) {
			$this->purge_url( $url );
		}
	}

	/**
	 * Purge a term archive (and its parent's, if hierarchical).
	 *
	 * @param int    $term_id
	 * @param string $taxonomy
	 */
	public function purge_term( $term_id, $taxonomy = '' ) {
		if ( empty( $taxonomy ) ) {
			$term = get_term( $term_id );
			if ( $term && ! is_wp_error( $term ) ) {
				$taxonomy = $term->taxonomy;
			}
		}
		if ( empty( $taxonomy ) ) {
			return;
		}
		$link = get_term_link( (int) $term_id, $taxonomy );
		if ( is_string( $link ) ) {
			$this->purge_url( $link );
		}
	}

	/* ------------------------------------------------------------------ */
	/*  Hook callbacks                                                     */
	/* ------------------------------------------------------------------ */

	public function on_save_post( $post_id, $post = null ) {
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}
		if ( null === $post ) {
			$post = get_post( $post_id );
		}
		if ( ! $post || 'publish' !== $post->post_status ) {
			// Status transitions handle moving from draft → publish; here we only
			// purge for currently-public posts.
			return;
		}
		$this->purge_post( $post_id );
	}

	public function on_deleted_post( $post_id ) {
		$this->purge_post( $post_id );
	}

	public function on_transition_post_status( $new, $old, $post ) {
		if ( $new === $old ) {
			return;
		}
		// publish ↔ anything OR private ↔ publish triggers a purge.
		if ( 'publish' === $new || 'publish' === $old ) {
			$this->purge_post( $post->ID );
		}
	}

	public function on_comment_change( $comment_id ) {
		$comment = get_comment( $comment_id );
		if ( ! $comment ) {
			return;
		}
		$this->purge_post( (int) $comment->comment_post_ID );
	}

	public function on_term_change( $term_id, $tt_id = 0, $taxonomy = '' ) {
		$this->purge_term( (int) $term_id, (string) $taxonomy );
		// Term changes can affect listing pages too; cheap insurance.
		$this->purge_url( home_url( '/' ) );
	}

	public function on_user_change( $user_id ) {
		$url = get_author_posts_url( (int) $user_id );
		if ( $url ) {
			$this->purge_url( $url );
		}
	}

	public function on_wc_product_change( $product ) {
		// $product may be a product object or an ID depending on the action.
		$id = is_object( $product ) && method_exists( $product, 'get_id' ) ? $product->get_id() : (int) $product;
		if ( $id ) {
			$this->purge_post( $id );
		}
		// Also purge shop/page-cache home for inventory changes.
		if ( function_exists( 'wc_get_page_id' ) ) {
			$shop_id = wc_get_page_id( 'shop' );
			if ( $shop_id > 0 ) {
				$this->purge_post( $shop_id );
			}
		}
	}

	/* ------------------------------------------------------------------ */
	/*  Internals                                                          */
	/* ------------------------------------------------------------------ */

	/**
	 * Collect every URL that may need invalidation when $post is updated.
	 *
	 * @param WP_Post $post
	 * @return string[] Unique list of absolute URLs.
	 */
	public function collect_urls_for_post( $post ) {
		$urls = array();

		// Permalink.
		$permalink = get_permalink( $post );
		if ( is_string( $permalink ) ) {
			$urls[] = $permalink;
		}

		// Home / front page.
		$urls[] = home_url( '/' );
		$urls[] = trailingslashit( home_url() );

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

		// Date-based archives.
		$year  = get_year_link( get_the_date( 'Y', $post ) );
		$month = get_month_link( get_the_date( 'Y', $post ), get_the_date( 'm', $post ) );
		$day   = get_day_link( get_the_date( 'Y', $post ), get_the_date( 'm', $post ), get_the_date( 'd', $post ) );
		foreach ( array( $year, $month, $day ) as $link ) {
			if ( is_string( $link ) ) {
				$urls[] = $link;
			}
		}

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

		// All taxonomies attached to this post type.
		$taxonomies = get_object_taxonomies( $post->post_type );
		foreach ( (array) $taxonomies as $tax ) {
			$terms = wp_get_post_terms( $post->ID, $tax );
			if ( is_array( $terms ) ) {
				foreach ( $terms as $term ) {
					$tlink = get_term_link( $term );
					if ( is_string( $tlink ) ) {
						$urls[] = $tlink;
					}
				}
			}
		}

		// Filter so third-parties can extend (e.g. WC product cat is already covered
		// by the taxonomy loop above; this is the hook for custom listing URLs).
		$urls = (array) apply_filters( 'blaminhor_essentials_cache_purge_post_urls', $urls, $post );

		return array_values( array_unique( array_filter( $urls ) ) );
	}
}
