<?php
/**
 * `wp powersuite module` commands.
 *
 * @package WP_PowerSuite\CLI
 * @since 1.0.7
 */

namespace WP_PowerSuite\CLI;

use WP_PowerSuite\CLI\Support\Cli;
use WP_PowerSuite\CLI\Support\SensitiveModules;
use WP_PowerSuite\Core\ModuleActivationService;
use WP_PowerSuite\Core\OptionStore;

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

/**
 * List, inspect, enable, and disable WP PowerSuite modules.
 *
 * @since 1.0.7
 */
final class ModuleCommand {

	/**
	 * List all WP PowerSuite modules and their state.
	 *
	 * ## OPTIONS
	 *
	 * [--enabled]
	 * : Only show enabled modules.
	 *
	 * [--disabled]
	 * : Only show disabled modules.
	 *
	 * [--category=<category>]
	 * : Filter by category. Accepts the display name ("Login & Users") or a
	 *   slug ("login-users", "admin", "seo-visibility"); matching is
	 *   case-insensitive.
	 *
	 * [--fields=<fields>]
	 * : Comma-separated list of fields to display.
	 * ---
	 * default: slug,name,category,enabled,tier,available
	 * ---
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - csv
	 *   - json
	 *   - yaml
	 *   - count
	 *   - ids
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp powersuite module list
	 *     $ wp powersuite module list --enabled --format=json
	 *     $ wp powersuite module list --category=admin
	 *     $ wp powersuite module list --category="Login & Users"
	 *
	 * @subcommand list
	 * @when after_wp_load
	 *
	 * @param array<int, string>   $args       Positional arguments (unused).
	 * @param array<string, mixed> $assoc_args Associative arguments.
	 * @return void
	 */
	public function list_( array $args, array $assoc_args ): void {
		$registry     = Cli::registry();
		$all          = $registry->getAllModules();
		$enabled_list = OptionStore::getInstance()->getEnabledModules();

		$only_enabled  = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'enabled', false );
		$only_disabled = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'disabled', false );
		$category      = (string) \WP_CLI\Utils\get_flag_value( $assoc_args, 'category', '' );
		$category_norm = self::normalizeCategory( $category );

		// Map normalized slug => display label for every category in use, so we
		// can accept slugs ("login-users") or labels ("Login & Users") and warn
		// on typos with the real options.
		$known_categories = [];
		foreach ( $all as $meta ) {
			$label = (string) ( $meta['category'] ?? '' );
			if ( $label !== '' ) {
				$known_categories[ self::normalizeCategory( $label ) ] = $label;
			}
		}

		if ( $category_norm !== '' && ! isset( $known_categories[ $category_norm ] ) ) {
			\WP_CLI::warning(
				sprintf(
					/* translators: 1: requested category, 2: comma-separated list of valid categories */
					__( 'Unknown category "%1$s". Available categories: %2$s', 'wp-powersuite' ),
					$category,
					implode( ', ', array_values( $known_categories ) )
				)
			);
		}

		$rows = [];
		foreach ( $all as $module_id => $meta ) {
			$is_enabled = in_array( $module_id, $enabled_list, true );

			if ( $only_enabled && ! $is_enabled ) {
				continue;
			}
			if ( $only_disabled && $is_enabled ) {
				continue;
			}
			if ( $category_norm !== '' && self::normalizeCategory( (string) ( $meta['category'] ?? '' ) ) !== $category_norm ) {
				continue;
			}

			$rows[] = [
				'slug'      => (string) $module_id,
				'name'      => (string) ( $meta['name'] ?? $module_id ),
				'category'  => (string) ( $meta['category'] ?? 'uncategorized' ),
				'enabled'   => Cli::yesNo( $is_enabled ),
				'tier'      => ! empty( $meta['is_pro'] ) ? 'pro' : 'free',
				'available' => Cli::yesNo( Cli::isModuleAvailable( $meta ) ),
				'context'   => is_array( $meta['context'] ?? null ) ? implode( '+', $meta['context'] ) : (string) ( $meta['context'] ?? 'both' ),
			];
		}

		usort( $rows, static fn( $a, $b ) => strcmp( $a['slug'], $b['slug'] ) );

		$fields    = [ 'slug', 'name', 'category', 'enabled', 'tier', 'available', 'context' ];
		$formatter = new \WP_CLI\Formatter( $assoc_args, $fields );
		$formatter->display_items( $rows );
	}

	/**
	 * Normalize a category value to a comparable slug.
	 *
	 * Lowercases and collapses any run of non-alphanumeric characters into a
	 * single hyphen so display labels and slugs compare equal, e.g.
	 * "Login & Users", "login & users", and "login-users" all map to
	 * "login-users".
	 *
	 * @param string $value Raw category label or slug.
	 * @return string
	 */
	private static function normalizeCategory( string $value ): string {
		$value = strtolower( trim( $value ) );
		$value = (string) preg_replace( '/[^a-z0-9]+/', '-', $value );
		return trim( $value, '-' );
	}

	/**
	 * Show detailed information about a single module.
	 *
	 * ## OPTIONS
	 *
	 * <slug>
	 * : The module slug.
	 *
	 * [--format=<format>]
	 * : Render output in a particular format.
	 * ---
	 * default: table
	 * options:
	 *   - table
	 *   - json
	 *   - yaml
	 * ---
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp powersuite module get redirect-manager
	 *     $ wp powersuite module get broken-link-checker --format=json
	 *
	 * @when after_wp_load
	 *
	 * @param array<int, string>   $args       Positional arguments.
	 * @param array<string, mixed> $assoc_args Associative arguments.
	 * @return void
	 */
	public function get( array $args, array $assoc_args ): void {
		$slug = isset( $args[0] ) ? (string) $args[0] : '';
		$meta = Cli::moduleOrError( $slug );

		$module_id   = (string) ( $meta['id'] ?? sanitize_key( $slug ) );
		$has_config  = ! empty( OptionStore::getInstance()->get( $module_id, [] ) );
		$context     = is_array( $meta['context'] ?? null ) ? implode( '+', $meta['context'] ) : (string) ( $meta['context'] ?? 'both' );
		$requires    = is_array( $meta['requires'] ?? null ) ? $meta['requires'] : [];

		$data = [
			'slug'             => $module_id,
			'name'             => (string) ( $meta['name'] ?? $module_id ),
			'description'      => (string) ( $meta['description'] ?? '' ),
			'category'         => (string) ( $meta['category'] ?? 'uncategorized' ),
			'version'          => (string) ( $meta['version'] ?? '' ),
			'tier'             => ! empty( $meta['is_pro'] ) ? 'pro' : 'free',
			'enabled'          => Cli::isEnabled( $module_id ),
			'available'        => Cli::isModuleAvailable( $meta ),
			'context'          => $context,
			'has_settings'     => $has_config,
			'has_config_page'  => ! empty( $meta['has_advanced_settings'] ) || ! empty( $meta['has_custom_page'] ),
			'dependencies'     => implode( ', ', (array) ( $meta['dependencies'] ?? [] ) ),
			'conflicts'        => implode( ', ', (array) ( $meta['conflicts'] ?? [] ) ),
			'requires_php'     => (string) ( $requires['php'] ?? '' ),
			'requires_wp'      => (string) ( $requires['wordpress'] ?? '' ),
		];

		Cli::renderKeyValue( $data, $assoc_args );
	}

	/**
	 * Enable one or more modules.
	 *
	 * Enabling an already-enabled module is a no-op (idempotent). Premium
	 * modules require a valid license; dependency and conflict rules match the
	 * admin UI exactly.
	 *
	 * ## OPTIONS
	 *
	 * [<slug>...]
	 * : One or more module slugs to enable.
	 *
	 * [--all]
	 * : Enable every available module. Sensitive modules (login URL, maintenance
	 *   mode, REST/XML-RPC toggles, SMTP routing, federated auth, etc.) are
	 *   skipped unless --include-sensitive is also passed.
	 *
	 * [--include-sensitive]
	 * : With --all, also enable modules normally excluded for safety.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp powersuite module enable redirect-manager
	 *     $ wp powersuite module enable redirect-manager broken-link-checker
	 *     $ wp powersuite module enable --all
	 *     $ wp powersuite module enable --all --include-sensitive
	 *
	 * @when after_wp_load
	 *
	 * @param array<int, string>   $args       Positional arguments (module slugs).
	 * @param array<string, mixed> $assoc_args Associative arguments.
	 * @return void
	 */
	public function enable( array $args, array $assoc_args ): void {
		$all = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false );

		if ( $all ) {
			$this->enableAll( (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'include-sensitive', false ) );
			return;
		}

		if ( $args === [] ) {
			\WP_CLI::error( __( 'Specify one or more module slugs, or use --all.', 'wp-powersuite' ) );
		}

		$failed = 0;
		foreach ( $args as $slug ) {
			if ( ! $this->enableOne( (string) $slug ) ) {
				++$failed;
			}
		}

		if ( $failed > 0 ) {
			\WP_CLI::error(
				sprintf(
					/* translators: %d: number of modules that failed to enable */
					_n( '%d module could not be enabled.', '%d modules could not be enabled.', $failed, 'wp-powersuite' ),
					$failed
				)
			);
		}

		\WP_CLI::success( __( 'Done.', 'wp-powersuite' ) );
	}

	/**
	 * Disable one or more modules.
	 *
	 * Disabling an already-disabled module is a no-op (idempotent).
	 *
	 * ## OPTIONS
	 *
	 * [<slug>...]
	 * : One or more module slugs to disable.
	 *
	 * [--all]
	 * : Disable every currently enabled module.
	 *
	 * ## EXAMPLES
	 *
	 *     $ wp powersuite module disable redirect-manager
	 *     $ wp powersuite module disable --all
	 *
	 * @when after_wp_load
	 *
	 * @param array<int, string>   $args       Positional arguments (module slugs).
	 * @param array<string, mixed> $assoc_args Associative arguments.
	 * @return void
	 */
	public function disable( array $args, array $assoc_args ): void {
		$all = (bool) \WP_CLI\Utils\get_flag_value( $assoc_args, 'all', false );

		if ( $all ) {
			$args = OptionStore::getInstance()->getEnabledModules();
			if ( $args === [] ) {
				\WP_CLI::success( __( 'No modules are enabled.', 'wp-powersuite' ) );
				return;
			}
		}

		if ( $args === [] ) {
			\WP_CLI::error( __( 'Specify one or more module slugs, or use --all.', 'wp-powersuite' ) );
		}

		$failed = 0;
		foreach ( $args as $slug ) {
			if ( ! $this->disableOne( (string) $slug ) ) {
				++$failed;
			}
		}

		if ( $failed > 0 ) {
			\WP_CLI::error(
				sprintf(
					/* translators: %d: number of modules that failed to disable */
					_n( '%d module could not be disabled.', '%d modules could not be disabled.', $failed, 'wp-powersuite' ),
					$failed
				)
			);
		}

		\WP_CLI::success( __( 'Done.', 'wp-powersuite' ) );
	}

	/**
	 * Enable a single module with full validation. Returns false on failure.
	 *
	 * @param string $slug Module slug.
	 * @return bool
	 */
	private function enableOne( string $slug ): bool {
		$meta      = Cli::moduleOrError( $slug );
		$module_id = (string) ( $meta['id'] ?? sanitize_key( $slug ) );

		if ( Cli::isEnabled( $module_id ) ) {
			\WP_CLI::log( sprintf( '%s: already enabled.', $module_id ) );
			return true;
		}

		if ( ! Cli::isModuleAvailable( $meta ) ) {
			\WP_CLI::warning( sprintf( '%s: requires a valid license and was not enabled.', $module_id ) );
			return false;
		}

		$activation = ModuleActivationService::getInstance();
		$enabled    = OptionStore::getInstance()->getEnabledModules();
		$validation = $activation->validateEnable( $module_id, $enabled, $meta );

		if ( ! $validation['allowed'] ) {
			if ( $validation['code'] === 'missing_dependencies' ) {
				\WP_CLI::warning(
					sprintf(
						'%s: enable required dependency first: %s',
						$module_id,
						implode( ', ', $validation['missing_dependencies'] )
					)
				);
			} else {
				\WP_CLI::warning(
					sprintf(
						'%s: conflicts with %s. Disable the conflicting module(s) first.',
						$module_id,
						implode( ', ', $validation['conflicts'] )
					)
				);
			}
			return false;
		}

		if ( ! $activation->applyToggle( $module_id, true, $meta ) ) {
			\WP_CLI::warning( sprintf( '%s: failed to persist enable.', $module_id ) );
			return false;
		}

		\WP_CLI::log( sprintf( '%s: enabled.', $module_id ) );
		return true;
	}

	/**
	 * Disable a single module. Returns false on failure.
	 *
	 * @param string $slug Module slug.
	 * @return bool
	 */
	private function disableOne( string $slug ): bool {
		$meta      = Cli::moduleOrError( $slug );
		$module_id = (string) ( $meta['id'] ?? sanitize_key( $slug ) );

		if ( ! Cli::isEnabled( $module_id ) ) {
			\WP_CLI::log( sprintf( '%s: already disabled.', $module_id ) );
			return true;
		}

		if ( ! ModuleActivationService::getInstance()->applyToggle( $module_id, false, $meta ) ) {
			\WP_CLI::warning( sprintf( '%s: failed to persist disable.', $module_id ) );
			return false;
		}

		\WP_CLI::log( sprintf( '%s: disabled.', $module_id ) );
		return true;
	}

	/**
	 * Enable every available module, honoring dependency ordering and the
	 * sensitive-module policy.
	 *
	 * @param bool $include_sensitive Whether to include sensitive modules.
	 * @return void
	 */
	private function enableAll( bool $include_sensitive ): void {
		$registry = Cli::registry();
		$all      = $registry->getAllModules();

		$candidates = [];
		$skipped    = [];
		foreach ( $all as $module_id => $meta ) {
			$module_id = (string) $module_id;

			if ( Cli::isEnabled( $module_id ) ) {
				continue;
			}
			if ( ! Cli::isModuleAvailable( $meta ) ) {
				$skipped[ $module_id ] = 'license';
				continue;
			}
			if ( ! $include_sensitive && SensitiveModules::isSensitive( $module_id ) ) {
				$skipped[ $module_id ] = 'sensitive';
				continue;
			}
			$candidates[ $module_id ] = $meta;
		}

		foreach ( $skipped as $module_id => $reason ) {
			if ( $reason === 'sensitive' ) {
				\WP_CLI::log( sprintf( '%s: skipped (sensitive; use --include-sensitive to force).', $module_id ) );
			} else {
				\WP_CLI::log( sprintf( '%s: skipped (requires a valid license).', $module_id ) );
			}
		}

		if ( $candidates === [] ) {
			\WP_CLI::success( __( 'No modules to enable.', 'wp-powersuite' ) );
			return;
		}

		// Iteratively enable modules whose dependencies are already satisfied so
		// ordering never blocks a valid set.
		$activation = ModuleActivationService::getInstance();
		$enabled    = OptionStore::getInstance()->getEnabledModules();
		$count      = 0;

		do {
			$progress = false;
			foreach ( $candidates as $module_id => $meta ) {
				$validation = $activation->validateEnable( $module_id, $enabled, $meta );
				if ( ! $validation['allowed'] ) {
					continue;
				}
				if ( $activation->applyToggle( $module_id, true, $meta ) ) {
					\WP_CLI::log( sprintf( '%s: enabled.', $module_id ) );
					$enabled[] = $module_id;
					++$count;
				}
				unset( $candidates[ $module_id ] );
				$progress = true;
			}
		} while ( $progress && $candidates !== [] );

		foreach ( array_keys( $candidates ) as $module_id ) {
			\WP_CLI::warning( sprintf( '%s: skipped (unmet dependencies or conflicts).', $module_id ) );
		}

		\WP_CLI::success(
			sprintf(
				/* translators: %d: number of modules enabled */
				_n( 'Enabled %d module.', 'Enabled %d modules.', $count, 'wp-powersuite' ),
				$count
			)
		);
	}
}
