<?php
/**
 * Dual-layer uninstall cleanup.
 *
 * Default: remove credentials / connection secrets and operational leftovers
 * (cron and rate-limit transients) while preserving SEO
 * settings, meta, redirects, and generated files.
 *
 * Full wipe (when seovaultseo_erase_on_uninstall is enabled): remove all plugin
 * options, meta, custom tables, CPT content, uploads cache, and related files.
 *
 * NotFound (404 fallback / recovery) policy:
 * - Settings live under seovaultseo_notfound_* and are removed only on full wipe
 *   (covered by the seovaultseo_ option-name prefix — see get_notfound_option_keys()).
 * - No custom DB tables for this feature.
 * - User-created pages referenced by notfound_recovery_page_id or
 *   notfound_fallback_custom_page_id are never deleted (we never auto-create them).
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Core;

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

/**
 * Runs from uninstall.php (plugin classes may be unavailable — keep WP-API only).
 */
class Uninstaller {

	/**
	 * Option that gates full data erasure.
	 */
	const ERASE_OPTION = 'seovaultseo_erase_on_uninstall';

	/**
	 * Run uninstall cleanup.
	 *
	 * @return void
	 */
	public static function run() {
		$erase_all = self::should_erase_all_data();

		// Always clear secrets + operational leftovers (safe on accidental delete).
		self::clear_credentials();
		self::clear_auth_transients();
		self::clear_cron_events();

		if ( ! $erase_all ) {
			return;
		}

		self::erase_all_data();
	}

	/**
	 * Whether the user opted into full data erasure.
	 *
	 * @return bool
	 */
	public static function should_erase_all_data() {
		$value = get_option( self::ERASE_OPTION, 'no' );
		return in_array( $value, array( 'yes', '1', 1, true ), true );
	}

	/**
	 * Remove API connection secrets (always).
	 *
	 * @return void
	 */
	private static function clear_credentials() {
		$credential_options = array(
			'seovaultseo_site_key',
			'seovaultseo_site_read_key',
			'seovaultseo_site_write_key',
			'seovaultseo_connected_at',
			'seovaultseo_connection_verified',
			// Legacy connector keys.
			'seovaultai_site_key',
			'seovaultai_connected_at',
			'seovaultai_connection_verified',
		);

		foreach ( $credential_options as $option ) {
			delete_option( $option );
		}
	}

	/**
	 * Clear auth / flash / rate-limit related transients (always).
	 *
	 * @return void
	 */
	private static function clear_auth_transients() {
		delete_transient( 'seovaultseo_flash_site_read_key' );
		delete_transient( 'seovaultseo_flash_site_write_key' );
		delete_transient( 'seovaultseo_activated' );

		self::delete_transients_by_prefix( 'seovault_rl_' );
	}

	/**
	 * Unschedule plugin cron events (always — callbacks cannot run after delete).
	 *
	 * @return void
	 */
	private static function clear_cron_events() {
		wp_clear_scheduled_hook( 'seovaultseo_sitemap_rebuild' );
	}

	/**
	 * Full wipe of SEO data, settings, tables, files, and CPT content.
	 *
	 * @return void
	 */
	private static function erase_all_data() {
		self::delete_indexnow_key_file();
		self::drop_custom_tables();
		self::delete_plugin_options();
		self::delete_plugin_meta();
		self::delete_plugin_transients();
		self::delete_plugin_user_meta();
		self::delete_plugin_posts_and_terms();
		self::delete_uploads_cache_directory();

		flush_rewrite_rules();
	}

	/**
	 * Delete IndexNow verification file from ABSPATH when present.
	 *
	 * @return void
	 */
	private static function delete_indexnow_key_file() {
		$key = (string) get_option( 'seovaultseo_indexnow_key', '' );
		if ( '' === $key || ! preg_match( '/^[a-f0-9]{8,128}$/i', $key ) ) {
			return;
		}

		$path = trailingslashit( ABSPATH ) . $key . '.txt';
		if ( file_exists( $path ) ) {
			wp_delete_file( $path );
		}
	}

	/**
	 * Custom table names owned by this plugin (with $wpdb->prefix applied).
	 *
	 * NotFound fallback/recovery does not own a table — only Monitor404 / Redirect
	 * Manager / API audit do.
	 *
	 * @return string[]
	 */
	public static function get_custom_table_names() {
		global $wpdb;

		return array(
			$wpdb->prefix . 'seovaultseo_404_logs',
			$wpdb->prefix . 'seovaultseo_redirects',
			$wpdb->prefix . 'seovault_api_audit',
		);
	}

	/**
	 * WordPress option names for NotFound (404 fallback + recovery) settings.
	 *
	 * Full wipe deletes these via the seovaultseo_ LIKE pass in delete_plugin_options().
	 * Default uninstall leaves them intact. Keys are listed explicitly for audits/tests.
	 *
	 * @return string[] Full option_name values (including seovaultseo_ prefix).
	 */
	public static function get_notfound_option_keys() {
		$suffixes = array(
			'notfound_fallback_mode',
			'notfound_fallback_status',
			'notfound_fallback_custom_url',
			'notfound_fallback_custom_page_id',
			'notfound_log_redirected_fallbacks',
			'notfound_recovery_enabled',
			'notfound_recovery_headline',
			'notfound_recovery_message',
			'notfound_recovery_show_search',
			'notfound_recovery_show_home_cta',
			'notfound_recovery_cta_label',
			'notfound_recovery_cta_url',
			'notfound_recovery_show_recent',
			'notfound_recovery_recent_count',
			'notfound_recovery_use_theme_colors',
			'notfound_recovery_accent',
			'notfound_recovery_content_mode',
			'notfound_recovery_page_id',
		);

		$keys = array();
		foreach ( $suffixes as $suffix ) {
			$keys[] = 'seovaultseo_' . $suffix;
		}

		return $keys;
	}

	/**
	 * Drop custom database tables.
	 *
	 * @return void
	 */
	private static function drop_custom_tables() {
		/**
		 * Whether uninstall may DROP custom tables.
		 *
		 * Disabled in PHPUnit: DDL causes an implicit MySQL commit that breaks
		 * WP_UnitTestCase SAVEPOINT rollback.
		 *
		 * @param bool $drop Whether to drop tables. Default true.
		 */
		if ( ! apply_filters( 'seovaultseo_uninstall_drop_tables', true ) ) {
			return;
		}

		global $wpdb;

		foreach ( self::get_custom_table_names() as $table ) {
			// Validate table name characters before interpolating into DDL.
			if ( ! preg_match( '/^[A-Za-z0-9_]+$/', $table ) ) {
				continue;
			}

			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQL.NotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Uninstall DROP TABLE; table name validated above and cannot use prepare() placeholders for identifiers.
			$wpdb->query( 'DROP TABLE IF EXISTS ' . $table );
		}
	}

	/**
	 * Delete all plugin options (including the erase toggle).
	 *
	 * @return void
	 */
	private static function delete_plugin_options() {
		global $wpdb;

		$like_patterns = array(
			$wpdb->esc_like( 'seovaultseo_' ) . '%',
			$wpdb->esc_like( 'seovaultai_' ) . '%',
		);

		foreach ( $like_patterns as $like ) {
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Uninstall cleanup.
			$names = $wpdb->get_col(
				$wpdb->prepare(
					"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s",
					$like
				)
			);

			if ( ! is_array( $names ) ) {
				continue;
			}

			foreach ( $names as $name ) {
				delete_option( $name );
			}
		}

		delete_option( 'seovault_locations_slug' );
		delete_option( self::ERASE_OPTION );
	}

	/**
	 * Delete post / term meta with the plugin prefix.
	 *
	 * @return void
	 */
	private static function delete_plugin_meta() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s",
				$wpdb->esc_like( '_seovault_' ) . '%'
			)
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->termmeta} WHERE meta_key LIKE %s",
				$wpdb->esc_like( '_seovault_' ) . '%'
			)
		);

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
				$wpdb->esc_like( '_seovault_' ) . '%'
			)
		);
	}

	/**
	 * Delete remaining plugin transients on full wipe.
	 *
	 * @return void
	 */
	private static function delete_plugin_transients() {
		self::delete_transients_by_prefix( 'seovaultseo_' );
		self::delete_transients_by_prefix( 'seovault_rl_' );
	}

	/**
	 * Delete review / notice user meta on full wipe.
	 *
	 * @return void
	 */
	private static function delete_plugin_user_meta() {
		global $wpdb;

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$wpdb->query(
			$wpdb->prepare(
				"DELETE FROM {$wpdb->usermeta} WHERE meta_key LIKE %s",
				$wpdb->esc_like( 'seovaultseo_' ) . '%'
			)
		);
	}

	/**
	 * Delete Local SEO / schema CPT content and location taxonomy terms.
	 *
	 * @return void
	 */
	private static function delete_plugin_posts_and_terms() {
		$post_types = array( 'seovault_locations', 'seovault_schema' );

		foreach ( $post_types as $post_type ) {
			$ids = get_posts(
				array(
					'post_type'      => $post_type,
					'post_status'    => 'any',
					'posts_per_page' => -1,
					'fields'         => 'ids',
					'nopaging'       => true,
				)
			);

			if ( ! is_array( $ids ) ) {
				continue;
			}

			foreach ( $ids as $post_id ) {
				wp_delete_post( (int) $post_id, true );
			}
		}

		$terms = get_terms(
			array(
				'taxonomy'   => 'seovault_locations_category',
				'hide_empty' => false,
				'fields'     => 'ids',
			)
		);

		if ( is_wp_error( $terms ) || ! is_array( $terms ) ) {
			return;
		}

		foreach ( $terms as $term_id ) {
			wp_delete_term( (int) $term_id, 'seovault_locations_category' );
		}
	}

	/**
	 * Remove sitemap / plugin cache directory under uploads.
	 *
	 * @return void
	 */
	private static function delete_uploads_cache_directory() {
		$upload_dir = wp_upload_dir();
		if ( ! empty( $upload_dir['error'] ) || empty( $upload_dir['basedir'] ) ) {
			return;
		}

		$cache_dir = trailingslashit( $upload_dir['basedir'] ) . 'seovault-seo';
		self::rmdir_recursive( $cache_dir );
	}

	/**
	 * Delete all transients whose key starts with a prefix.
	 *
	 * @param string $prefix Transient key prefix (without _transient_).
	 * @return void
	 */
	private static function delete_transients_by_prefix( $prefix ) {
		global $wpdb;

		$like = $wpdb->esc_like( '_transient_' . $prefix ) . '%';

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
		$names = $wpdb->get_col(
			$wpdb->prepare(
				"SELECT option_name FROM {$wpdb->options}
				WHERE option_name LIKE %s
				  AND option_name NOT LIKE %s",
				$like,
				$wpdb->esc_like( '_transient_timeout_' ) . '%'
			)
		);

		if ( is_array( $names ) ) {
			foreach ( $names as $option_name ) {
				$key = (string) $option_name;
				if ( 0 === strpos( $key, '_transient_' ) ) {
					$key = substr( $key, strlen( '_transient_' ) );
				}
				delete_transient( $key );
			}
		}

		// 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( '_transient_timeout_' . $prefix ) . '%'
			)
		);
	}

	/**
	 * Recursively delete a directory.
	 *
	 * @param string $dir Absolute directory path.
	 * @return void
	 */
	private static function rmdir_recursive( $dir ) {
		$dir = wp_normalize_path( untrailingslashit( $dir ) );
		if ( '' === $dir || ! is_dir( $dir ) ) {
			return;
		}

		$items = scandir( $dir );
		if ( ! is_array( $items ) ) {
			return;
		}

		foreach ( $items as $item ) {
			if ( '.' === $item || '..' === $item ) {
				continue;
			}

			$path = $dir . '/' . $item;
			if ( is_dir( $path ) ) {
				self::rmdir_recursive( $path );
			} else {
				wp_delete_file( $path );
			}
		}

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_rmdir -- Uninstall cleanup of plugin upload cache.
		rmdir( $dir );
	}
}
