<?php
/**
 * Shared uninstall cleanup: drops the plugin's tables, options, capability and
 * scheduled events. Called from two places so it runs no matter how the plugin
 * is removed:
 *
 *   - the free build's uninstall.php (WordPress fires it standalone), and
 *   - the Freemius `after_uninstall` hook (the pro build ships no uninstall.php,
 *     because the Freemius deployment processor requires it be absent).
 *
 * The function is idempotent, so running it twice is harmless.
 *
 * @package Stocktake_For_WooCommerce
 */

defined( 'ABSPATH' ) || exit;

if ( ! function_exists( 'stwc_sibling_install_exists' ) ) {
	/**
	 * Is another on-disk install of this plugin (the separately distributed
	 * free or pro build) present besides THIS one? Used by both the uninstall
	 * cleanup and the deactivation hook so removing one copy never destroys
	 * the shared data/cron of the other. Realpath-normalized: a symlinked
	 * sole install can never read as its own sibling.
	 *
	 * @return bool
	 */
	function stwc_sibling_install_exists() {
		if ( ! defined( 'WP_PLUGIN_DIR' ) ) {
			return false;
		}
		$stwc_self = realpath( dirname( __DIR__ ) );
		if ( ! $stwc_self ) {
			return false;
		}
		$stwc_mains = glob( WP_PLUGIN_DIR . '/*/stocktake-for-woocommerce.php' );
		if ( ! is_array( $stwc_mains ) ) {
			// glob() can return false on error; (array) false would yield
			// array(false) and a CWD-dependent dirname(false) comparison.
			return false;
		}
		foreach ( $stwc_mains as $stwc_main ) {
			$stwc_dir = realpath( dirname( $stwc_main ) );
			if ( $stwc_dir && $stwc_dir !== $stwc_self && file_exists( $stwc_dir . '/includes/class-stwc-sessions.php' ) ) {
				return true;
			}
		}
		return false;
	}
}

if ( ! function_exists( 'stwc_uninstall_cleanup' ) ) {
	/**
	 * Remove every trace of the plugin from the database.
	 */
	function stwc_uninstall_cleanup() {
		// Free/Pro co-install: the separately distributed Pro build shares the
		// same tables, options and cron hooks. If a SIBLING install of this
		// plugin still exists on disk, deleting THIS copy must not destroy the
		// other one's data — skip the cleanup entirely and let the last copy
		// standing do it. Paths are realpath-normalized so a symlinked sole
		// install can never mistake itself for a sibling.
		if ( function_exists( 'stwc_sibling_install_exists' ) && stwc_sibling_install_exists() ) {
			return;
		}

		// Multisite: the five tables, the options, the role caps and the cron
		// events are ALL per site -- $wpdb->prefix, wp_roles() and the cron array
		// each follow switch_to_blog(). Uninstall runs once, so without this loop
		// a network keeps every other site's tables and rows forever.
		$stwc_site_cleanup = static function () {
			global $wpdb;

			$stwc_tables = array(
				$wpdb->prefix . 'stwc_log',
				$wpdb->prefix . 'stwc_backup_items',
				$wpdb->prefix . 'stwc_backups',
				$wpdb->prefix . 'stwc_items',
				$wpdb->prefix . 'stwc_sessions',
			);

			// phpcs:disable WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
			foreach ( $stwc_tables as $stwc_table ) {
				$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $stwc_table ) );
			}
			// phpcs:enable

			// Options by PREFIX, not by a hand-kept list. The list had already fallen
			// behind the code it was meant to describe: it still named a key that no
			// longer exists (stwc_review_dismissed) while the two the review engine
			// writes survived uninstall. Every option this plugin writes carries the
			// stwc_ prefix, so ask the database instead of remembering.
			// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- uninstall-scope enumeration by prefix.
			$stwc_options = (array) $wpdb->get_col(
				$wpdb->prepare( "SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s", $wpdb->esc_like( 'stwc_' ) . '%' )
			);
			foreach ( $stwc_options as $stwc_option ) {
				// delete_option(), not a bulk DELETE: it clears the options cache too,
				// which a raw query would leave holding rows that no longer exist.
				delete_option( $stwc_option );
			}

			// Per-session rows written by prefix: apply-mutex options (raw INSERT,
			// left behind when an apply crashes) and the apply-error transients.
			// phpcs:disable WordPress.DB.DirectDatabaseQuery
			$wpdb->query(
				$wpdb->prepare(
					'DELETE FROM %i WHERE option_name LIKE %s OR option_name LIKE %s OR option_name LIKE %s',
					$wpdb->options,
					$wpdb->esc_like( 'stwc_apply_lock_' ) . '%',
					$wpdb->esc_like( '_transient_stwc_apply_err_' ) . '%',
					$wpdb->esc_like( '_transient_timeout_stwc_apply_err_' ) . '%'
				)
			);
			// phpcs:enable

			// Remove the counting capability from every role.
			if ( function_exists( 'wp_roles' ) ) {
				foreach ( wp_roles()->role_objects as $role ) {
					if ( $role->has_cap( 'stwc_count' ) ) {
						$role->remove_cap( 'stwc_count' );
					}
				}
			}

			// Per-user language preference meta (set by the plugin's own switcher).
			delete_metadata( 'user', 0, 'stwc_locale', '', true );

			wp_clear_scheduled_hook( 'stwc_reminder_event' );
			wp_clear_scheduled_hook( 'stwc_populate' );

			wp_cache_flush(); // The direct DELETE bypassed the options cache.
		};

		if ( is_multisite() ) {
			$stwc_site_ids = get_sites(
				array(
					'fields' => 'ids',
					'number' => 0,
				)
			);
			foreach ( $stwc_site_ids as $stwc_site_id ) {
				switch_to_blog( (int) $stwc_site_id );
				$stwc_site_cleanup();
				restore_current_blog();
			}
		} else {
			$stwc_site_cleanup();
		}
	}
}
