<?php
/**
 * Uninstall routine.
 *
 * Deliberately standalone: it uses only core APIs and $wpdb so it still works if
 * the Composer autoloader is missing at uninstall time.
 *
 * @package TextTango
 */

declare( strict_types=1 );

defined( 'WP_UNINSTALL_PLUGIN' ) || exit;

/**
 * Remove every trace of the plugin from one site.
 */
/**
 * Remove the plugin's meta from every order.
 *
 * Batched rather than loaded in one go: a store being uninstalled may have a
 * hundred thousand orders, and reading them all at once is how an uninstall
 * turns into a fatal that leaves the job half done.
 */
function texttango_uninstall_order_meta(): void {
	if ( ! function_exists( 'wc_get_orders' ) ) {
		return;
	}

	$keys = array(
		'_texttango_contact_uuid',
		'_texttango_sms_optin',
		'_texttango_sms_optin_at',
		'_texttango_sms_optin_source',
		'_texttango_tracking_sent',
	);

	$page = 1;

	do {
		$orders = wc_get_orders(
			array(
				'limit'   => 200,
				'page'    => $page,
				'orderby' => 'ID',
				'order'   => 'ASC',
				'status'  => 'any',
				'return'  => 'objects',
			)
		);

		foreach ( (array) $orders as $order ) {
			if ( ! is_object( $order ) || ! method_exists( $order, 'delete_meta_data' ) ) {
				continue;
			}

			$touched = false;

			foreach ( $keys as $key ) {
				if ( '' !== (string) $order->get_meta( $key, true ) ) {
					$order->delete_meta_data( $key );
					$touched = true;
				}
			}

			if ( $touched ) {
				$order->save();
			}
		}

		++$page;
	} while ( ! empty( $orders ) );
}

function texttango_uninstall_site(): void {
	global $wpdb;

	$settings = get_option( 'texttango_settings' );

	// The merchant asked us to leave their delivery history and settings alone.
	if ( is_array( $settings ) && ! empty( $settings['keep_data_on_uninstall'] ) ) {
		return;
	}

	// `texttango_campaigns` is both a table and, on a site upgraded from before
	// DB version 7, an option that held the same history. Both go: the option is
	// still listed below, because a site that never loaded an admin page after
	// upgrading still has it.
	foreach ( array( 'texttango_messages', 'texttango_suppressions', 'texttango_consents', 'texttango_audit', 'texttango_campaigns', 'texttango_campaign_recipients' ) as $suffix ) {
		$table = $wpdb->prefix . $suffix;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, PluginCheck.Security.DirectDB.UnescapedDBParameter -- Table names cannot be parameterised; these are literals joined to $wpdb->prefix.
		$wpdb->query( "DROP TABLE IF EXISTS {$table}" );
	}

	$options = array(
		'texttango_settings',
		'texttango_api_key',
		'texttango_webhook_secret',
		'texttango_db_version',
		'texttango_connection_state',
		'texttango_reconcile_report',
		'texttango_webhook_state',
		'texttango_balance',
		'texttango_circuit',
		'texttango_campaigns',
		'texttango_health_alerted',
		'texttango_setup_dismissed',
		'texttango_forms',
	);

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

	$meta_keys = array(
		'texttango_phone',
		'texttango_phone_verified_at',
		'texttango_contact_uuid',
		'texttango_contact_synced_at',
		'texttango_sms_optin',
		'texttango_sms_optin_at',
		'texttango_sms_optin_source',
		'texttango_2fa_enabled',
		'texttango_cart_reminded',
	);

	foreach ( $meta_keys as $meta_key ) {
		delete_metadata( 'user', 0, $meta_key, '', true );
	}

	/*
	 * Order and product meta, which nothing here used to touch at all. Under
	 * High-Performance Order Storage an order's meta lives in its own table
	 * rather than in wp_postmeta, so a query against posts would miss every
	 * store on the default configuration since WooCommerce 8.2 — and what it
	 * would miss is `_texttango_sms_optin*`, a customer's consent.
	 *
	 * Deleted through WooCommerce's own CRUD so both storage backends are
	 * covered by the same code.
	 */
	texttango_uninstall_order_meta();

	/*
	 * Product and coupon meta is ordinary post meta whatever WooCommerce is
	 * doing with orders, so one delete per key clears it across every post at
	 * once.
	 *
	 * The coupons themselves are deliberately left alone. `_texttango_campaign`
	 * and `_texttango_recipient` mark a discount code that was texted to a
	 * named customer, and that code is a promise the shop made — deleting the
	 * coupon because the plugin was removed would revoke a discount somebody is
	 * still holding. The bookkeeping goes; the promise stays until it expires.
	 */
	foreach ( array( '_texttango_stock_alerted', '_texttango_campaign', '_texttango_recipient' ) as $meta_key ) {
		delete_metadata( 'post', 0, $meta_key, '', true );
	}

	// OTP challenges and throttle counters.
	// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- One-off cleanup; no caching layer applies during uninstall.
	$transients = $wpdb->get_col(
		$wpdb->prepare(
			"SELECT option_name FROM {$wpdb->options} WHERE option_name LIKE %s OR option_name LIKE %s",
			$wpdb->esc_like( '_transient_texttango_' ) . '%',
			$wpdb->esc_like( '_transient_timeout_texttango_' ) . '%'
		)
	);

	foreach ( (array) $transients as $option_name ) {
		delete_option( $option_name );
	}

	$roles = wp_roles();

	foreach ( array_keys( $roles->roles ) as $role_name ) {
		$role = get_role( $role_name );

		if ( $role instanceof WP_Role ) {
			$role->remove_cap( 'manage_texttango' );
		}
	}
}

if ( is_multisite() ) {
	$texttango_site_ids = get_sites(
		array(
			'fields' => 'ids',
			'number' => 0,
		)
	);

	foreach ( $texttango_site_ids as $texttango_site_id ) {
		switch_to_blog( (int) $texttango_site_id );
		texttango_uninstall_site();
		restore_current_blog();
	}
} else {
	texttango_uninstall_site();
}
