<?php
/**
 * Cache module — Conflict detector
 *
 * Detects other active page-cache plugins that would clash with our drop-in.
 * Refusing activation when one is found is safer than letting two caches
 * fight (results: stale serves, missing cookies, broken purges).
 *
 * @package BlaminhorEssentials
 */

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

class Blaminhor_Essentials_Cache_Conflict_Detector {

	/**
	 * Map of plugin slug => display name for known cache plugins.
	 *
	 * Note: LiteSpeed Cache is intentionally listed too. Even though it ships
	 * with some hosts by default, running it side-by-side with our page cache
	 * results in double-caching and corrupted purges. The user must pick one.
	 *
	 * @return array<string, string>
	 */
	public function get_known_cache_plugins() {
		return array(
			'wp-rocket/wp-rocket.php'                       => 'WP Rocket',
			'w3-total-cache/w3-total-cache.php'             => 'W3 Total Cache',
			'wp-super-cache/wp-cache.php'                   => 'WP Super Cache',
			'litespeed-cache/litespeed-cache.php'           => 'LiteSpeed Cache',
			'wp-fastest-cache/wpFastestCache.php'           => 'WP Fastest Cache',
			'cache-enabler/cache-enabler.php'               => 'Cache Enabler',
			'breeze/breeze.php'                             => 'Breeze',
			'hummingbird-performance/wp-hummingbird.php'    => 'Hummingbird',
			'sg-cachepress/sg-cachepress.php'               => 'SG Optimizer',
			'comet-cache/comet-cache.php'                   => 'Comet Cache',
			'powered-cache/powered-cache.php'               => 'Powered Cache',
			'autoptimize/autoptimize.php'                   => 'Autoptimize',
			'wp-optimize/wp-optimize.php'                   => 'WP-Optimize',
		);
	}

	/**
	 * Return the list of currently-active cache plugins. Empty array = no conflict.
	 *
	 * Considers both single-site active_plugins AND network-active plugins on
	 * multisite installs (active_sitewide_plugins).
	 *
	 * @return array<int, array{slug: string, name: string}> List of conflicts.
	 */
	public function detect_active_conflicts() {
		$known  = $this->get_known_cache_plugins();
		$active = $this->collect_active_plugins();

		$conflicts = array();
		foreach ( $known as $slug => $name ) {
			if ( in_array( $slug, $active, true ) ) {
				$conflicts[] = array(
					'slug' => $slug,
					'name' => $name,
				);
			}
		}

		// A foreign advanced-cache.php drop-in by itself is NOT a blocking
		// conflict. WP Rocket / W3TC / etc. routinely leave their drop-in
		// behind after deactivation, and the installer takes them over
		// (with backup) on our activation. See detect_foreign_dropin().

		return $conflicts;
	}

	/**
	 * Detect a non-Blaminhor advanced-cache.php drop-in. Used for diagnostics
	 * (Health Check, admin notices) but NOT for blocking activation: the
	 * installer takes over a foreign drop-in with a timestamped backup.
	 *
	 * @return array|null Null when none, or {path, signature} when present.
	 */
	public function detect_foreign_dropin() {
		$advanced_cache = WP_CONTENT_DIR . '/advanced-cache.php';
		if ( ! file_exists( $advanced_cache ) || $this->is_our_dropin( $advanced_cache ) ) {
			return null;
		}
		return array(
			'path'      => $advanced_cache,
			'signature' => $this->guess_dropin_origin( $advanced_cache ),
		);
	}

	/**
	 * Decide whether this module should suspend its HTML optimisation hooks.
	 *
	 * Suspension is reserved for the case we genuinely cannot work around:
	 * **another cache plugin is currently active**. Both wiring up their
	 * subscribers means double `script_loader_tag`, double output-buffer,
	 * conflicting purges — the kind of bug that produced anonymous-only
	 * broken renders in production.
	 *
	 * A *foreign drop-in* alone (e.g. residue from WP Rocket disabled but
	 * not deleted) is NOT a suspension reason: we own it, back it up, and
	 * install our own. See `Module_Cache::maybe_take_over_foreign_dropin()`.
	 *
	 * @return bool
	 */
	public function should_suspend_optimizations() {
		return ! empty( $this->detect_active_conflicts() );
	}

	/**
	 * Helper used by registration code: returns the human reason for
	 * suspension (or empty string when nothing is wrong). Foreign-drop-in-
	 * only state returns empty — that case is auto-resolved by takeover,
	 * not surfaced as a suspension to the user.
	 *
	 * @return string Empty when no suspension; otherwise translated reason.
	 */
	public function get_suspension_reason() {
		$conflicts = $this->detect_active_conflicts();
		if ( ! empty( $conflicts ) ) {
			return sprintf(
				/* translators: %s: comma-separated list of conflicting plugin names */
				__( 'Another cache plugin is active: %s.', 'blaminhor-essentials' ),
				$this->format_conflict_list( $conflicts )
			);
		}
		return '';
	}

	/**
	 * Whether we should silently take over a foreign drop-in. True when:
	 *   - no active cache plugin conflict (we're the only cache in town), AND
	 *   - the wp-content/advanced-cache.php on disk is not ours.
	 *
	 * @return bool
	 */
	public function needs_dropin_takeover() {
		if ( ! empty( $this->detect_active_conflicts() ) ) {
			return false;
		}
		return null !== $this->detect_foreign_dropin();
	}

	/**
	 * Best-effort origin guess for a foreign drop-in based on signatures
	 * commonly found in the first 4KB.
	 *
	 * @param string $path
	 * @return string Display name of the originating plugin, or "unknown".
	 */
	private function guess_dropin_origin( $path ) {
		$head = @file_get_contents( $path, false, null, 0, 4096 );
		if ( false === $head ) {
			return 'unknown';
		}
		$signatures = array(
			'WP Rocket'        => array( 'WP Rocket', 'WP_ROCKET_', 'rocket-cache' ),
			'W3 Total Cache'   => array( 'W3 Total Cache', 'W3TC_', 'w3-total-cache' ),
			'WP Super Cache'   => array( 'WP Super Cache', 'wpsupercache' ),
			'LiteSpeed Cache'  => array( 'LiteSpeed' ),
			'WP Fastest Cache' => array( 'WpFastestCache', 'wp-fastest-cache' ),
			'Cache Enabler'    => array( 'Cache_Enabler' ),
			'Hummingbird'      => array( 'Hummingbird', 'wphb' ),
			'Breeze'           => array( 'Breeze' ),
			'SG Optimizer'     => array( 'sg-cachepress', 'SG_Optimizer' ),
			'Comet Cache'      => array( 'Comet Cache' ),
			'Powered Cache'    => array( 'Powered Cache' ),
		);
		foreach ( $signatures as $name => $needles ) {
			foreach ( $needles as $needle ) {
				if ( false !== stripos( $head, $needle ) ) {
					return $name;
				}
			}
		}
		return 'unknown';
	}

	/**
	 * Format a human-readable list of conflicts (translated names, comma-separated).
	 *
	 * @param array $conflicts Output of detect_active_conflicts().
	 * @return string
	 */
	public function format_conflict_list( $conflicts ) {
		$names = wp_list_pluck( $conflicts, 'name' );
		return implode( ', ', $names );
	}

	/**
	 * Collect all active plugin slugs across single-site and network scopes.
	 *
	 * @return array<int, string>
	 */
	private function collect_active_plugins() {
		$active = (array) get_option( 'active_plugins', array() );

		if ( is_multisite() ) {
			$network_active = (array) get_site_option( 'active_sitewide_plugins', array() );
			// active_sitewide_plugins keys are plugin slugs, values are timestamps.
			$active = array_merge( $active, array_keys( $network_active ) );
		}

		return array_unique( $active );
	}

	/**
	 * Detect whether an existing drop-in file was put there by us (not someone else).
	 *
	 * Our drop-in always carries the marker `BLAMINHOR_ESSENTIALS_DROPIN` near the top.
	 *
	 * @param string $path Absolute path to advanced-cache.php.
	 * @return bool
	 */
	private function is_our_dropin( $path ) {
		$head = @file_get_contents( $path, false, null, 0, 4096 );
		if ( false === $head ) {
			return false;
		}
		return false !== strpos( $head, 'BLAMINHOR_ESSENTIALS_DROPIN' );
	}
}
