<?php
/**
 * Object-cache-safe transient purging.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.0
 */

namespace WP_PowerSuite\Core;

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

/**
 * Deletes transients through the WordPress transient API instead of raw
 * `DELETE FROM wp_options` queries, so external object caches (Redis,
 * Memcached, etc.) stay consistent with the database.
 *
 * When an external object cache is active, transients often live only in
 * the cache and never get a row in wp_options. Callers should therefore:
 * 1. {@see remember()} keys as they are created, and
 * 2. {@see deleteRemembered()} + {@see deleteByPrefix()} on cleanup.
 */
final class TransientPurger {

	/**
	 * Remember a transient name in a durable registry so it can be purged
	 * later under an external object cache.
	 *
	 * @param string $registry_option Option name holding the key list.
	 * @param string $key             Transient name (no `_transient_` prefix).
	 * @param int    $max_keys        Soft cap to bound option growth.
	 */
	public static function remember( string $registry_option, string $key, int $max_keys = 5000 ): void {
		$registry_option = sanitize_key( $registry_option );
		$key             = (string) $key;
		if ( $registry_option === '' || $key === '' ) {
			return;
		}

		$keys = get_option( $registry_option, [] );
		if ( ! is_array( $keys ) ) {
			$keys = [];
		}

		if ( in_array( $key, $keys, true ) ) {
			return;
		}

		$keys[] = $key;
		if ( count( $keys ) > $max_keys ) {
			$keys = array_slice( $keys, (int) ( -$max_keys ) );
		}

		update_option( $registry_option, array_values( $keys ), false );
	}

	/**
	 * Delete every transient remembered in a registry, then remove the registry.
	 *
	 * @param string $registry_option Option name holding the key list.
	 * @return int Number of successful delete_transient() calls.
	 */
	public static function deleteRemembered( string $registry_option ): int {
		$registry_option = sanitize_key( $registry_option );
		if ( $registry_option === '' ) {
			return 0;
		}

		$keys = get_option( $registry_option, [] );
		delete_option( $registry_option );

		return self::deleteKeys( is_array( $keys ) ? $keys : [] );
	}

	/**
	 * Delete every transient/site transient whose name starts with
	 * $transient_prefix, discovered via wp_options (and wp_sitemeta on
	 * multisite for site transients).
	 *
	 * @param string $transient_prefix Transient name prefix, without the
	 *                                 `_transient_` / `_site_transient_` storage prefix.
	 * @return int Number of delete_transient()/delete_site_transient() calls that returned true.
	 */
	public static function deleteByPrefix( string $transient_prefix ): int {
		$transient_prefix = trim( $transient_prefix );
		if ( $transient_prefix === '' ) {
			return 0;
		}

		global $wpdb;
		$deleted = 0;

		$like = $wpdb->esc_like( '_transient_' . $transient_prefix ) . '%';
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- discovering names for deletion through the transient API below.
		$names = $wpdb->get_col(
			$wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $like )
		);

		foreach ( $names as $name ) {
			if ( ! is_string( $name ) || str_starts_with( $name, '_transient_timeout_' ) ) {
				continue;
			}
			if ( delete_transient( substr( $name, strlen( '_transient_' ) ) ) ) {
				++$deleted;
			}
		}

		if ( is_multisite() ) {
			$like_site = $wpdb->esc_like( '_site_transient_' . $transient_prefix ) . '%';
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- discovering names for deletion through the transient API below.
			$site_names = $wpdb->get_col(
				$wpdb->prepare( "SELECT meta_key FROM {$wpdb->sitemeta} WHERE meta_key LIKE %s", $like_site )
			);

			foreach ( $site_names as $name ) {
				if ( ! is_string( $name ) || str_starts_with( $name, '_site_transient_timeout_' ) ) {
					continue;
				}
				if ( delete_site_transient( substr( $name, strlen( '_site_transient_' ) ) ) ) {
					++$deleted;
				}
			}
		}

		return $deleted;
	}

	/**
	 * Delete a fixed list of transient names via delete_transient(). Use this
	 * for keys derived from a known set of identifiers (IPs, user IDs, etc.)
	 * so they are purged even when an external object cache means they never
	 * had a row in wp_options.
	 *
	 * @param array<int, string> $keys Transient names (without the `_transient_` storage prefix).
	 * @return int Number of delete_transient() calls that returned true.
	 */
	public static function deleteKeys( array $keys ): int {
		$deleted = 0;
		foreach ( $keys as $key ) {
			if ( ! is_string( $key ) || $key === '' ) {
				continue;
			}
			if ( delete_transient( $key ) ) {
				++$deleted;
			}
		}

		return $deleted;
	}
}
