<?php
/**
 * Desinstalación limpia de Entrepo — vía el hook `after_uninstall` de Freemius.
 *
 * F7 (Freemius): Freemius NO admite un `uninstall.php` suelto en un plugin que
 * usa su SDK (él registra su propio `register_uninstall_hook` para su limpieza
 * y luego dispara `after_uninstall`). La lógica de desinstalación vive por eso
 * aquí, en una clase autoloadable, registrada con
 * `entrepo_fs()->add_action( 'after_uninstall', [ Uninstaller::class, 'run' ] )`
 * en `entrepo.php`. Freemius carga el plugin (autoloader incluido) durante la
 * desinstalación, así que esta clase está disponible cuando el hook dispara.
 *
 * DOS REGLAS DE ORO, intocables (ver también el FAQ de `readme.txt`):
 *
 * 1. Las metas de PEDIDO NUNCA se tocan: `_entrepo_nota_fiscal`,
 *    `_entrepo_vies_request_id`, `_entrepo_vies_fecha`, `_entrepo_aviso_fiscal`
 *    (escritas por `WooCommerce\FiscalHooks::aplicar_exencion()`) e
 *    `is_vat_exempt` (meta NATIVA de WooCommerce que este plugin solo LEE). Son
 *    el registro fiscal de facturas ya emitidas — obligación de conservación.
 *    Esta clase JAMÁS ejecuta un DELETE contra pedidos ni contra las tablas de
 *    pedidos de WooCommerce (HPOS `wp_wc_orders_meta` o legacy `wp_postmeta`
 *    con `post_type` `shop_order*`): el único DELETE de postmeta usa
 *    `meta_key LIKE '_entrepo_precio_%'`, un prefijo que NINGUNA meta de pedido
 *    usa jamás — coincide solo con overrides de precio de PRODUCTOS/variaciones.
 *
 * 2. Las clases y tipos de impuesto sembrados por `Fiscal\ConfiguradorIva`
 *    (21/10/4/0% + recargo) NO se borran: son configuración fiscal de la TIENDA
 *    (no de Entrepo) que puede seguir en uso por pedidos históricos o productos.
 *
 * Se borra: la tabla `{prefix}entrepo_precios`; las options del sitio; las user
 * metas (globales a la red); los overrides `_entrepo_precio_<role>`; los roles
 * `entrepo_pendiente` y `entrepo_g_*` (reasignando a `customer` a quien se quede
 * sin rol); y las acciones de Action Scheduler propias.
 *
 * Multisite: los datos per-sitio (tabla/options/postmeta/roles) se borran por
 * cada blog con `switch_to_blog()`; las user metas son globales (una tabla
 * `wp_usermeta` compartida) y se borran UNA vez, fuera del bucle. Idempotente y
 * sin fatales: cada paso comprueba antes de actuar.
 *
 * Deliberadamente usa SOLO funciones nativas de WP (ninguna otra clase del
 * plugin), para no depender de que el resto del plugin siga operativo.
 *
 * @package Entrepo
 */

namespace Entrepo\Core;

defined( 'ABSPATH' ) || exit;

/**
 * Rutina de desinstalación de Entrepo (invocada por el hook `after_uninstall`).
 */
final class Uninstaller {

	/**
	 * Punto de entrada: recorre todos los sitios (multisite) o el único sitio
	 * activo (single-site) borrando los datos per-sitio, y borra al final las
	 * user meta (globales a la red) una sola vez.
	 */
	public static function run(): void {
		if ( is_multisite() ) {
			$sitios_red = get_sites( array( 'fields' => 'ids' ) );
			foreach ( $sitios_red as $sitio_id ) {
				switch_to_blog( (int) $sitio_id );
				self::borrar_datos_del_sitio();
				restore_current_blog();
			}
		} else {
			self::borrar_datos_del_sitio();
		}

		self::borrar_user_meta();
	}

	/**
	 * Borra todo el rastro de datos de Entrepo que es PER-SITIO en el sitio
	 * ACTUAL: tabla de precios, roles, post meta de overrides y options.
	 */
	private static function borrar_datos_del_sitio(): void {
		global $wpdb;

		// 1. Roles PRIMERO: lee la opción `entrepo_grupos` (borrada en el paso 4).
		self::purgar_roles();

		// 2. Tabla de precios precalculada (matriz producto/variación/rol).
		// esc_sql() es un no-op sobre este literal (viene de $wpdb->prefix, no de
		// entrada de usuario), pero marca el nombre como escapado para el análisis
		// estático de Plugin Check, que no puede seguir el origen del valor.
		$tabla = esc_sql( $wpdb->prefix . 'entrepo_precios' );
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange, WordPress.DB.PreparedSQL.InterpolatedNotPrepared -- desinstalación: tabla propia del plugin, sin equivalente en la API de WP; un DROP TABLE no admite placeholders para nombres de tabla; {$tabla} viene de $wpdb->prefix (sin entrada de usuario).
		$wpdb->query( "DROP TABLE IF EXISTS {$tabla}" );

		// 3. Post meta de overrides de precio (`_entrepo_precio_<role>`). Ver la
		// Regla de Oro 1: este prefijo NUNCA coincide con una meta de PEDIDO.
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- desinstalación: no hay wrapper de WP para "borrar toda meta cuyo key empiece por X"; esc_like() + %s cubre el escapado.
		$wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE meta_key LIKE %s", $wpdb->esc_like( '_entrepo_precio_' ) . '%' ) );

		// El DELETE directo no invalida el object cache de meta; en sitios con
		// cache persistente un get_post_meta() posterior devolvería el valor
		// viejo. Flush completo — aceptable: la desinstalación es rarísima.
		wp_cache_flush();

		// 4. Options propias del sitio.
		$options = array(
			'entrepo_grupos',
			'entrepo_db_version',
			'entrepo_ajustes_fiscales',
			'entrepo_ajustes_visibilidad',
			'entrepo_recalc_estado',
		);
		foreach ( $options as $option_name ) {
			delete_option( $option_name );
		}

		// 5. Action Scheduler: cancela las acciones pendientes de los dos jobs
		// propios. AS puede no estar disponible (por robustez no se asume).
		if ( function_exists( 'as_unschedule_all_actions' ) ) {
			as_unschedule_all_actions( 'entrepo_recalc_lote' );
			as_unschedule_all_actions( 'entrepo_vies_validar' );
		}
	}

	/**
	 * Purga `entrepo_pendiente` y todos los roles de grupo B2B (`entrepo_g_*`)
	 * del sitio ACTUAL. Los slugs se leen de `entrepo_grupos` ANTES de que
	 * `borrar_datos_del_sitio()` la borre; como red de seguridad, también barre
	 * TODOS los roles cuyo nombre empiece por `entrepo_g_`.
	 */
	private static function purgar_roles(): void {
		$grupos_guardados = get_option( 'entrepo_grupos', array() );
		$slugs            = is_array( $grupos_guardados ) ? array_keys( $grupos_guardados ) : array();

		foreach ( array_keys( wp_roles()->roles ) as $role_slug ) {
			if ( 0 === strpos( $role_slug, 'entrepo_g_' ) && ! in_array( $role_slug, $slugs, true ) ) {
				$slugs[] = $role_slug;
			}
		}

		$slugs[] = 'entrepo_pendiente';

		foreach ( $slugs as $role_slug ) {
			self::purgar_rol( $role_slug );
		}
	}

	/**
	 * Reasigna a `customer` (si se queda sin ningún rol) y borra un rol concreto
	 * creado por Entrepo, en el sitio ACTUAL. Réplica simplificada de
	 * `Grupos\GrupoRepository::eliminar()` con solo funciones nativas de WP.
	 *
	 * @param string $role_slug Slug del rol a purgar.
	 */
	private static function purgar_rol( string $role_slug ): void {
		if ( null === get_role( $role_slug ) ) {
			return; // Ya no existe (desinstalación repetida, o nunca se creó).
		}

		$usuarios = get_users(
			array(
				'role'   => $role_slug,
				'fields' => 'ID',
			)
		);
		foreach ( $usuarios as $user_id ) {
			$user = new \WP_User( $user_id );
			$user->remove_role( $role_slug );
			if ( empty( $user->roles ) ) {
				$user->add_role( 'customer' );
			}
		}

		remove_role( $role_slug );
	}

	/**
	 * Borra las user meta de Entrepo — GLOBALES a toda la red (una única tabla
	 * `wp_usermeta`), por eso se llama UNA sola vez, fuera del bucle de sitios.
	 */
	private static function borrar_user_meta(): void {
		$claves = array(
			'entrepo_razon_social',
			'entrepo_nif_iva',
			'entrepo_pais',
			'entrepo_aprobado_en',
			'entrepo_recargo',
			'entrepo_vies_estado',
			'entrepo_vies_fecha',
			'entrepo_vies_request_id',
			'entrepo_vies_nombre',
			'entrepo_vies_encolado_ts',
		);

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