<?php
/**
 * Validated registry for plugin-owned runtime data.
 *
 * Optional modules register their own identifiers while present. Shared lifecycle
 * code can then clean up a downgraded installation without knowing module details.
 *
 * @package VoidPulse
 */

namespace VoidPulse\Core;

defined( 'ABSPATH' ) || exit;

final class OwnershipRegistry {
	const OPTION = 'voidpulse_runtime_ownership';

	/**
	 * Scopes awaiting Action Scheduler initialization.
	 *
	 * @var array<string,bool>
	 */
	private static $deferred_scopes = array();

	/**
	 * Register the identifiers owned by the core runtime.
	 *
	 * @return void
	 */
	public static function register_base() {
		self::register(
			'base',
			array(
				'tables'       => array( 'voidpulse_events' ),
				'options'      => array(
					'voidpulse_version',
					'voidpulse_schema_version',
					'voidpulse_defaults',
					'voidpulse_notification',
					'voidpulse_delete_data_on_uninstall',
					'voidpulse_activation_pending',
					'voidpulse_settings',
					'voidpulse_analytics_daily',
					'voidpulse_last_cache_refresh',
					'voidpulse_downgrade_checked',
					'voidpulse_refresh_lock',
				),
				'hooks'        => array( 'voidpulse_refresh_events', 'voidpulse_cleanup_events' ),
				'transients'   => self::rate_limit_transients(),
				'capabilities' => array(
					'manage_voidpulse',
					'edit_voidpulse_campaigns',
					'publish_voidpulse_campaigns',
					'view_voidpulse_analytics',
					'export_voidpulse_analytics',
					'manage_voidpulse_settings',
				),
				'action_groups' => array( 'voidpulse' ),
			)
		);
	}

	/**
	 * Return the fixed set of public rate-limit bucket transients.
	 *
	 * @return array<string>
	 */
	private static function rate_limit_transients() {
		$transients = array();
		for ( $bucket = 0; $bucket < \VoidPulse\Frontend\RestController::RATE_BUCKETS; $bucket++ ) {
			$transients[] = \VoidPulse\Frontend\RestController::rate_bucket_key( $bucket );
		}
		return $transients;
	}

	/**
	 * Register identifiers for one closed ownership scope.
	 *
	 * @param string                    $scope Scope.
	 * @param array<string,array<mixed>> $identifiers Identifiers by category.
	 * @return void
	 */
	public static function register( $scope, $identifiers ) {
		if ( ! in_array( $scope, array( 'base', 'optional' ), true ) || ! is_array( $identifiers ) ) {
			return;
		}
		$registry = self::get();
		foreach ( self::patterns() as $category => $pattern ) {
			$current = isset( $registry[ $scope ][ $category ] ) ? $registry[ $scope ][ $category ] : array();
			$values  = isset( $identifiers[ $category ] ) && is_array( $identifiers[ $category ] ) ? $identifiers[ $category ] : array();
			foreach ( $values as $value ) {
				if ( is_string( $value ) && preg_match( $pattern, $value ) ) {
					$current[] = $value;
				}
			}
			$registry[ $scope ][ $category ] = array_values( array_unique( $current ) );
		}
		update_option( self::OPTION, $registry, false );
	}

	/**
	 * Remove scheduled work belonging to a registered scope.
	 *
	 * @param string $scope Scope.
	 * @return void
	 */
	public static function unschedule( $scope ) {
		$registry = self::get();
		if ( empty( $registry[ $scope ] ) ) {
			return;
		}
		$groups = isset( $registry[ $scope ]['action_groups'] ) ? $registry[ $scope ]['action_groups'] : array( '' );
		if ( ! $groups ) {
			$groups = array( 'voidpulse' );
		}
		foreach ( $registry[ $scope ]['hooks'] as $hook ) {
			wp_clear_scheduled_hook( $hook );
			if ( function_exists( 'as_unschedule_all_actions' ) && did_action( 'action_scheduler_init' ) ) {
				foreach ( $groups as $group ) {
					as_unschedule_all_actions( $hook, array(), $group );
				}
			}
		}
		if ( function_exists( 'as_unschedule_all_actions' ) && ! did_action( 'action_scheduler_init' ) ) {
			self::$deferred_scopes[ $scope ] = true;
			add_action( 'action_scheduler_init', array( __CLASS__, 'run_deferred_unschedule' ), 20 );
		}
	}

	/**
	 * Finish Action Scheduler cleanup after its data store is ready.
	 *
	 * @return void
	 */
	public static function run_deferred_unschedule() {
		$scopes                = array_keys( self::$deferred_scopes );
		self::$deferred_scopes = array();
		foreach ( $scopes as $scope ) {
			self::unschedule( $scope );
		}
	}

	/**
	 * Return a stable signature for one validated ownership scope.
	 *
	 * @param string $scope Scope.
	 * @return string
	 */
	public static function signature( $scope ) {
		$registry = self::get();
		$data     = isset( $registry[ $scope ] ) ? $registry[ $scope ] : array();
		foreach ( $data as &$values ) {
			sort( $values );
		}
		unset( $values );
		return hash( 'sha256', wp_json_encode( $data ) );
	}

	/**
	 * Delete registered plugin-owned data for the current site.
	 *
	 * @return void
	 */
	public static function cleanup_current_site() {
		global $wpdb;

		if ( ! get_option( 'voidpulse_delete_data_on_uninstall', false ) ) {
			return;
		}
		$registry = self::get();
		foreach ( array_keys( $registry ) as $scope ) {
			self::unschedule( $scope );
			foreach ( $registry[ $scope ]['transients'] as $transient ) {
				delete_transient( $transient );
			}
			foreach ( $registry[ $scope ]['tables'] as $suffix ) {
				$table = $wpdb->prefix . $suffix;
				// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.DirectDatabaseQuery.SchemaChange -- Explicit opt-in uninstall cleanup of validated plugin-owned identifiers.
				$wpdb->query( $wpdb->prepare( 'DROP TABLE IF EXISTS %i', $table ) );
			}
			foreach ( $registry[ $scope ]['capabilities'] as $capability ) {
				foreach ( wp_roles()->role_names as $role_name => $label ) {
					unset( $label );
					$role = get_role( $role_name );
					if ( $role ) {
						$role->remove_cap( $capability );
					}
				}
			}
			foreach ( $registry[ $scope ]['options'] as $option ) {
				delete_option( $option );
			}
		}
		delete_option( self::OPTION );
	}

	/**
	 * Return a schema-complete, validated registry.
	 *
	 * @return array<string,array<string,array<string>>>
	 */
	private static function get() {
		$stored   = get_option( self::OPTION, array() );
		$registry = array();
		foreach ( array( 'base', 'optional' ) as $scope ) {
			foreach ( self::patterns() as $category => $pattern ) {
				$registry[ $scope ][ $category ] = array();
				$values = isset( $stored[ $scope ][ $category ] ) && is_array( $stored[ $scope ][ $category ] ) ? $stored[ $scope ][ $category ] : array();
				foreach ( $values as $value ) {
					if ( is_string( $value ) && preg_match( $pattern, $value ) ) {
						$registry[ $scope ][ $category ][] = $value;
					}
				}
			}
		}
		return $registry;
	}

	/**
	 * Closed identifier formats.
	 *
	 * @return array<string,string>
	 */
	private static function patterns() {
		return array(
			'tables'        => '/^voidpulse_[a-z0-9_]+$/',
			'options'       => '/^voidpulse_[a-z0-9_]+$/',
			'hooks'         => '/^voidpulse_[a-z0-9_]+$/',
			'transients'    => '/^voidpulse_[a-z0-9_]+$/',
			'capabilities'  => '/^[a-z]+_voidpulse(?:_[a-z0-9_]+)?$/',
			'action_groups' => '/^voidpulse$/',
		);
	}
}
