<?php
/**
 * Drift-proof data purge for uninstall.
 *
 * Defines the single prefix contract for all TrustLens-owned data and derives
 * every deletion from it, so new options/tables/meta/cron are removed
 * automatically without anyone maintaining a hand-written list.
 *
 * @package TrustLens
 * @since   1.3.2
 */

defined( 'ABSPATH' ) || exit;

/**
 * Prefix contract + purge routines used by trustlens_uninstall().
 *
 * @since 1.3.2
 */
class TrustLens_Data_Purge {

	/** Option-name prefixes owned by TrustLens. */
	private const OPTION_PREFIXES = array( 'trustlens_', '_trustlens_', 'wstl_' );

	/** User-meta key prefix owned by TrustLens. */
	private const META_PREFIX = 'trustlens_';

	/** Transient key prefixes owned by TrustLens. */
	private const TRANSIENT_PREFIXES = array( 'trustlens_', 'wstl_' );

	/** Custom-table base prefix (appended to $wpdb->prefix). */
	private const TABLE_PREFIX = 'trustlens_';

	/** WP-Cron hook namespace. */
	private const CRON_PREFIX = 'trustlens/';

	/**
	 * Action Scheduler action hooks. AS provides no prefix-based unschedule, so
	 * these are listed explicitly; they are low-churn and AS self-cleans
	 * orphaned actions anyway.
	 */
	private const AS_ACTION_HOOKS = array(
		'trustlens/calculate_score',
		'trustlens/sync_batch',
		'trustlens/sync_complete',
		'trustlens/send_scheduled_report',
		'trustlens/backfill_email_hash_meta',
		'trustlens/backfill_address_country_codes',
	);

	/**
	 * Escape LIKE wildcards the same way WordPress esc_like() does, without
	 * needing $wpdb so the pattern builders stay pure and unit-testable.
	 *
	 * @param string $text Raw literal.
	 * @return string
	 */
	public static function esc_like( string $text ): string {
		return addcslashes( $text, '_%\\' );
	}

	/**
	 * LIKE patterns matching every TrustLens option name.
	 *
	 * @return string[]
	 */
	public static function option_like_patterns(): array {
		return array_map(
			static function ( string $prefix ): string {
				return self::esc_like( $prefix ) . '%';
			},
			self::OPTION_PREFIXES
		);
	}

	/**
	 * LIKE pattern matching every TrustLens user-meta key.
	 *
	 * @return string
	 */
	public static function meta_like_pattern(): string {
		return self::esc_like( self::META_PREFIX ) . '%';
	}

	/**
	 * LIKE patterns matching every TrustLens transient (value + timeout rows).
	 *
	 * @return string[]
	 */
	public static function transient_like_patterns(): array {
		$patterns = array();
		foreach ( self::TRANSIENT_PREFIXES as $prefix ) {
			$escaped    = self::esc_like( $prefix );
			$patterns[] = '_transient_' . $escaped . '%';
			$patterns[] = '_transient_timeout_' . $escaped . '%';
		}
		return $patterns;
	}

	/**
	 * LIKE pattern matching every TrustLens custom table for a db prefix.
	 *
	 * @param string $wpdb_prefix The $wpdb->prefix value.
	 * @return string
	 */
	public static function table_like_pattern( string $wpdb_prefix ): string {
		return self::esc_like( $wpdb_prefix . self::TABLE_PREFIX ) . '%';
	}

	/**
	 * WP-Cron hook namespace prefix.
	 *
	 * @return string
	 */
	public static function cron_hook_prefix(): string {
		return self::CRON_PREFIX;
	}

	/**
	 * Clear all TrustLens scheduled work. Safe to run regardless of the
	 * data-removal opt-in — it never deletes stored data.
	 *
	 * @return void
	 */
	public static function purge_schedules(): void {
		// WP-Cron: unschedule every hook in the trustlens/ namespace (drift-proof).
		$crons = _get_cron_array();
		if ( is_array( $crons ) ) {
			$hooks = array();
			foreach ( $crons as $events ) {
				if ( ! is_array( $events ) ) {
					continue;
				}
				foreach ( array_keys( $events ) as $hook ) {
					if ( 0 === strpos( (string) $hook, self::CRON_PREFIX ) ) {
						$hooks[ $hook ] = true;
					}
				}
			}
			foreach ( array_keys( $hooks ) as $hook ) {
				wp_unschedule_hook( $hook );
			}
		}

		// Action Scheduler: the automation group holds only our dispatches; the
		// remaining AS hooks are listed explicitly (AS has no prefix unschedule).
		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( '', array(), 'trustlens-automation' );
			foreach ( self::AS_ACTION_HOOKS as $hook ) {
				as_unschedule_all_actions( $hook );
			}
		}
	}

	/**
	 * Permanently delete all stored TrustLens data via the prefix contract:
	 * custom tables, options, transients, and user-meta.
	 *
	 * @param \wpdb $wpdb WordPress database abstraction.
	 * @return void
	 */
	public static function purge_data( \wpdb $wpdb ): void {
		// Tables — discover by prefix so future tables are included.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- SHOW TABLES discovery, value is prepared.
		$tables = $wpdb->get_col( $wpdb->prepare( 'SHOW TABLES LIKE %s', self::table_like_pattern( $wpdb->prefix ) ) );
		foreach ( (array) $tables as $table ) {
			$safe = '`' . str_replace( '`', '``', $table ) . '`';
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- DROP TABLE requires direct query; identifier is backtick-escaped from a SHOW TABLES result.
			$wpdb->query( "DROP TABLE IF EXISTS {$safe}" );
		}

		// Options.
		$option_patterns = self::option_like_patterns();
		$option_where    = implode( ' OR ', array_fill( 0, count( $option_patterns ), 'option_name LIKE %s' ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Bulk prefix delete; the %s placeholders live inside $option_where and are prepared.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE {$option_where}", $option_patterns ) );

		// Transients — option names start with _transient_, so a separate query.
		$transient_patterns = self::transient_like_patterns();
		$transient_where    = implode( ' OR ', array_fill( 0, count( $transient_patterns ), 'option_name LIKE %s' ) );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare -- Bulk transient delete; the %s placeholders live inside $transient_where and are prepared.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE {$transient_where}", $transient_patterns ) );

		// User-meta.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Bulk prefix delete, pattern is prepared.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s", self::meta_like_pattern() ) );
	}
}
