<?php
/**
 * Module activation service.
 *
 * Canonical business logic for enabling and disabling WP PowerSuite modules.
 * Both the admin AJAX controller ({@see \WP_PowerSuite\Core\Http\Controllers\ModuleManagementController})
 * and the WP-CLI integration route through this service so validation, state
 * persistence, and post-toggle side effects can never drift between the two
 * interfaces.
 *
 * This service does NOT enforce capabilities or license gating for enable —
 * those are interface concerns (AJAX nonce/cap, CLI cap) handled by callers,
 * which keeps the persistence logic reusable and testable.
 *
 * @package WP_PowerSuite\Core
 * @since 1.0.7
 */

namespace WP_PowerSuite\Core;

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

/**
 * Enable/disable modules with shared validation and side effects.
 *
 * @since 1.0.7
 */
final class ModuleActivationService {

	private static ?ModuleActivationService $instance = null;

	public static function getInstance(): ModuleActivationService {
		if ( self::$instance === null ) {
			self::$instance = new self();
		}
		return self::$instance;
	}

	/**
	 * Validate whether a module may be enabled given the current enabled set.
	 *
	 * Checks unmet dependencies first, then blocking conflicts (forward and
	 * reverse). Returns structured data so each interface can render its own
	 * messaging while sharing the decision logic.
	 *
	 * @since 1.0.7
	 * @param string        $module_id    Module to enable.
	 * @param array<string> $enabled_list Currently enabled module IDs.
	 * @param array<string, mixed> $metadata Module metadata from the registry.
	 * @return array{allowed: bool, code: string, missing_dependencies: array<string>, conflicts: array<string>}
	 */
	public function validateEnable( string $module_id, array $enabled_list, array $metadata ): array {
		$dependencies = isset( $metadata['dependencies'] ) && is_array( $metadata['dependencies'] )
			? array_values( array_filter( array_map( 'sanitize_key', $metadata['dependencies'] ) ) )
			: [];

		$missing = array_values( array_diff( $dependencies, $enabled_list ) );
		if ( $missing !== [] ) {
			return [
				'allowed'              => false,
				'code'                 => 'missing_dependencies',
				'missing_dependencies' => $missing,
				'conflicts'            => [],
			];
		}

		$blocking = ConflictDetector::getInstance()->getBlockingConflictsForEnable( $module_id, $enabled_list );
		if ( $blocking !== [] ) {
			return [
				'allowed'              => false,
				'code'                 => 'module_conflict',
				'missing_dependencies' => [],
				'conflicts'            => $blocking,
			];
		}

		return [
			'allowed'              => true,
			'code'                 => '',
			'missing_dependencies' => [],
			'conflicts'            => [],
		];
	}

	/**
	 * Persist a module enable/disable and run the shared post-toggle side effects.
	 *
	 * Callers are responsible for validation (see {@see validateEnable()}) and
	 * authorization before invoking this. Persistence happens first; the
	 * {@see 'wp_powersuite_module_toggled'} action and side effects run only
	 * after a durable write, and a failing listener never fails the toggle.
	 *
	 * @since 1.0.7
	 * @param string               $module_id Module ID.
	 * @param bool                 $enable    True to enable, false to disable.
	 * @param array<string, mixed> $metadata  Module metadata (passed to the action).
	 * @return bool True when the enabled-modules list was persisted.
	 */
	public function applyToggle( string $module_id, bool $enable, array $metadata = [] ): bool {
		$store = OptionStore::getInstance();

		$success = $enable
			? $store->enableModule( $module_id )
			: $store->disableModule( $module_id );

		if ( ! $success ) {
			return false;
		}

		// Persist succeeded — keep the toggle successful even if a listener fatals.
		try {
			do_action( 'wp_powersuite_module_toggled', $module_id, $enable, $metadata );
			$this->fireToggleSideEffects( $module_id, $enable );
		} catch ( \Throwable $e ) {
			if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
				// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
				error_log(
					sprintf(
						'WP PowerSuite: module toggle side effect failed for "%s": %s',
						$module_id,
						$e->getMessage()
					)
				);
			}
		}

		return true;
	}

	/**
	 * Rewrite-flush and cache invalidation side effects tied to specific modules.
	 *
	 * Extracted from the AJAX controller so CLI toggles produce identical
	 * behavior (e.g. a redirect/link module enabled from CLI still schedules its
	 * rewrite flush).
	 *
	 * @since 1.0.7
	 * @param string $module_id Module ID.
	 * @param bool   $enable    Whether the module was enabled.
	 * @return void
	 */
	private function fireToggleSideEffects( string $module_id, bool $enable ): void {
		if ( $module_id === 'link-shortener' ) {
			set_transient( 'wpps_link_shortener_flush_rewrite', 1, 60 );
		}

		if ( $module_id === 'obfuscate-author-slugs' ) {
			set_transient( 'wpps_author_slugs_flush_rewrite', 1, MINUTE_IN_SECONDS );
			if ( ! $enable ) {
				delete_transient( 'wpps_author_slugs_rewrite_flushed' );
			}
		}

		if ( $module_id === 'disable-author-archives' ) {
			set_transient( 'wpps_disable_author_archives_flush_rewrite', 1, MINUTE_IN_SECONDS );
		}

		if ( $module_id === 'cloudflare-turnstile' && ! $enable ) {
			delete_transient( 'wpps_turnstile_cf_probe' );
		}
	}
}
