<?php
/**
 * Payment Page uninstall logic (called from `uninstall.php` at plugin root).
 *
 * Implements the 4-guard strict-mode safety check + the destructive
 * cleanup that runs when all guards pass. See `uninstall.php` for the
 * full guard-order rationale.
 *
 * @package PaymentPage
 * @since   1.5.1
 */

namespace PaymentPage;

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

class Uninstaller {

	/**
	 * The 5 DB tables PP creates. Listed explicitly (not derived) so the
	 * uninstall cleanup is deterministic regardless of plugin state at
	 * delete time. Keep in sync with `app/Migration.php::get_table_structure()`.
	 *
	 * @var string[]
	 */
	const TABLES = array(
		'payment_page_payments',
		'payment_page_log',
		'payment_page_stripe_customers',
		'payment_page_stripe_products',
		'payment_page_stripe_prices',
	);

	/**
	 * Run the 4-guard uninstall. Returns void; any guard failure logs +
	 * exits silently (WordPress doesn't surface uninstall.php errors to
	 * the admin UI).
	 */
	public static function run(): void {
		// Guard 2 — capability.
		if ( ! current_user_can( 'activate_plugins' ) ) {
			self::log( 'refused (capability check failed)' );
			return;
		}

		// Guard 3 — strict-mode setting (default: preserve).
		$settings = get_option( 'payment_page_settings', array() );
		$mode     = isset( $settings['uninstall_strict_mode'] ) ? (string) $settings['uninstall_strict_mode'] : 'preserve';
		if ( $mode !== 'destroy' ) {
			self::log( sprintf( 'refused (strict_mode=%s; default preserve). Set to "destroy" in Settings → Advanced → Uninstall behavior to allow.', $mode ) );
			return;
		}

		// Guard 4 — recent payment activity.
		$recent = self::recent_payment_count( 90 );
		if ( $recent > 0 ) {
			self::log( sprintf( 'refused (%d payments in last 90 days). Manually clear wp_payment_page_payments first or wait 90 days.', $recent ) );
			return;
		}

		// All guards passed — destroy.
		self::drop_tables();
		self::clear_options();
		self::clear_cpt_posts();
		self::clear_transients();
		self::log( 'completed (all 4 guards passed; tables dropped, options cleared, CPT posts deleted, transients flushed).' );
	}

	/**
	 * Count payment rows with a non-empty payment_intent_id in the last
	 * N days. Used by guard 4.
	 *
	 * @param int $days_back
	 * @return int Returns 0 on any DB error (fail-open is acceptable for
	 *             a count query; the destructive path is still gated by
	 *             guards 2 + 3).
	 */
	protected static function recent_payment_count( int $days_back ): int {
		global $wpdb;
		// Duck-typed availability check — `\wpdb` class isn't loaded in
		// pure-PHP unit tests so we can't rely on instanceof here.
		if ( ! isset( $wpdb ) || ! method_exists( $wpdb, 'get_var' ) || ! method_exists( $wpdb, 'prepare' ) ) {
			return 0;
		}
		$table     = $wpdb->prefix . 'payment_page_payments';
		$threshold = time() - ( $days_back * DAY_IN_SECONDS );
		// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- $table built from prefix + literal
		$sql = $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE payment_intent_id != '' AND created_at >= %d", $threshold );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- uninstall hook; no caching applicable
		$count = $wpdb->get_var( $sql );
		return is_numeric( $count ) ? (int) $count : 0;
	}

	/**
	 * Drop every PP-prefixed DB table.
	 */
	protected static function drop_tables(): void {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! method_exists( $wpdb, "query" ) ) {
			return;
		}
		foreach ( self::TABLES as $unprefixed ) {
			$table = $wpdb->prefix . $unprefixed;
			// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.SchemaChange,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
		}
	}

	/**
	 * Delete every PP `wp_options` row by name + autoload alias.
	 */
	protected static function clear_options(): void {
		$option_keys = array(
			'payment_page_settings',
			'payment_page_version',
			'payment_page_rewrite_flush_version',
			'payment_page_stripe_oauth_state',
			'payment_page_stripe_oauth_state_user',
			'payment_page_stripe_oauth_state_expires',
			'payment_page_form_templates',
			'payment_page_freemius_state',
			'payment_page_configuration-setup-rules-flushed',
		);
		foreach ( $option_keys as $k ) {
			delete_option( $k );
		}
		// Belt-and-suspenders: scrub anything else with the prefix.
		global $wpdb;
		if ( isset( $wpdb ) && method_exists( $wpdb, 'query' ) && method_exists( $wpdb, 'prepare' ) && method_exists( $wpdb, 'esc_like' ) ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
			$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'payment_page_' ) . '%' ) );
		}
	}

	/**
	 * Delete every `pp_payment_form` CPT post + its meta.
	 */
	protected static function clear_cpt_posts(): void {
		$posts = get_posts( array(
			'post_type'      => 'pp_payment_form',
			'post_status'    => 'any',
			'posts_per_page' => -1,
			'fields'         => 'ids',
		) );
		foreach ( (array) $posts as $post_id ) {
			wp_delete_post( (int) $post_id, true );
		}
	}

	/**
	 * Flush PP-prefixed transients (webhook idempotency dedupe, form-
	 * template cache, etc.).
	 */
	protected static function clear_transients(): void {
		global $wpdb;
		if ( ! isset( $wpdb ) || ! method_exists( $wpdb, "query" ) ) {
			return;
		}
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query( $wpdb->prepare(
			"DELETE FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
			'_transient_payment_page_%',
			'_transient_timeout_payment_page_%'
		) );
	}

	/**
	 * Single uniform log channel. Visible in debug.log when WP_DEBUG_LOG
	 * is enabled. (We deliberately don't surface to the admin UI —
	 * uninstall runs in an isolated request with no admin context.)
	 *
	 * @param string $message
	 */
	protected static function log( string $message ): void {
		// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- intentional uninstall-trail log
		error_log( 'Payment Page uninstall: ' . $message );
	}
}
