<?php

/**
 * PluginHandler — Domain handler for WordPress plugin management actions.
 *
 * Owns: audit_plugins, manage_plugins, update_plugin, search_plugins, get_plugin_versions, rollback_plugin
 *
 * Note: diagnose_seo and audit_security are complex diagnostic tools that
 * reference many site-wide concerns. They are intentionally left in
 * ActionExecutor for future extraction into a dedicated DiagnosticsHandler
 * in Phase 4.
 *
 * @package LolaCore\Actions\Handlers
 * @license GPL-2.0-or-later
 */

declare( strict_types=1 );

namespace LolaCore\Actions\Handlers;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Diagnostic exception messages; escaped at REST/chat presentation layer.

use LolaCore\Actions\Contracts\ActionHandlerInterface;

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

class PluginHandler implements ActionHandlerInterface {

	public function getSupportedActions(): array {
		return array( 'audit_plugins', 'toggle_plugin', 'update_plugin', 'search_plugins', 'install_plugin', 'bulk_update_plugins', 'get_plugin_versions', 'rollback_plugin' );
	}

	public function handle( string $actionId, array $params, bool $dryRun ): array {
		return match ( $actionId ) {
			'audit_plugins'  => $this->auditPlugins( $params ),
			'toggle_plugin'  => $this->managePlugins( $params, $dryRun ),
			'update_plugin'  => $this->updatePlugin( $params, $dryRun ),
			'search_plugins'      => $this->searchPlugins( $params ),
			'get_plugin_versions' => $this->getPluginVersions( $params ),
			'install_plugin'      => $this->installPlugin( $params, $dryRun ),
			'bulk_update_plugins' => $this->bulkUpdatePlugins( $params, $dryRun ),
			'rollback_plugin'     => $this->rollbackPlugin( $params, $dryRun ),
			default               => throw new \Exception( esc_html( "PluginHandler: unknown action '{$actionId}'." ) ),
		};
	}

	private function ensurePluginApiLoaded(): void {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}

	private function auditPlugins( array $params ): array {
		$this->ensurePluginApiLoaded();

		// Force fresh data.
		wp_clean_plugins_cache();
		delete_site_transient( 'update_plugins' );
		wp_update_plugins();

		$all_plugins = get_plugins();
		$active      = get_option( 'active_plugins', array() );
		$updates     = get_site_transient( 'update_plugins' );

		$result = array();
		foreach ( $all_plugins as $file => $data ) {
			$result[] = array(
				'slug'        => dirname( $file ),
				'name'        => $data['Name'] ?? '',
				'version'     => $data['Version'] ?? '',
				'is_active'   => in_array( $file, $active, true ),
				'has_update'  => isset( $updates->response[ $file ] ),
				'new_version' => isset( $updates->response[ $file ] ) ? $updates->response[ $file ]->new_version : null,
			);
		}
		return array( 'plugins' => $result );
	}

	private function managePlugins( array $params, bool $dryRun ): array {
		$action = $params['action'] ?? '';
		$slug   = $params['slug'] ?? '';

		if ( ! in_array( $action, array( 'activate', 'deactivate', 'delete' ), true ) ) {
			throw new \Exception( __( "Invalid action. Use 'activate', 'deactivate', or 'delete'.", 'lolacore' ) );
		}
		if ( empty( $slug ) ) {
			throw new \Exception( __( 'Plugin slug is required.', 'lolacore' ) );
		}

		$this->ensurePluginApiLoaded();
		$all_plugins = get_plugins();
		$plugin_file = null;
		foreach ( $all_plugins as $file => $data ) {
			if ( dirname( $file ) === $slug || $file === $slug ) {
				$plugin_file = $file;
				break;
			}
		}
		if ( ! $plugin_file ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( "Plugin with slug '%s' not found.", 'lolacore' ), $slug ) );
		}

		$is_active   = is_plugin_active( $plugin_file );
		$plugin_name = $all_plugins[ $plugin_file ]['Name'] ?? $slug;

		if ( $dryRun ) {
			return array(
				'action'      => "Will {$action} plugin",
				'slug'        => $slug,
				'plugin_name' => $plugin_name,
				'is_active'   => $is_active,
			);
		}

		if ( $action === 'activate' ) {
			$result = activate_plugin( $plugin_file );
			if ( is_wp_error( $result ) ) {
				throw new \Exception( esc_html( $result->get_error_message() ) );
			}
			/* translators: 1: value */
			return array( 'success' => true, 'message' => sprintf( __( "Plugin '%s' activated.", 'lolacore' ), $plugin_name ) );
		}

		if ( $action === 'deactivate' ) {
			deactivate_plugins( $plugin_file );
			/* translators: 1: value */
			return array( 'success' => true, 'message' => sprintf( __( "Plugin '%s' deactivated.", 'lolacore' ), $plugin_name ) );
		}

		// delete — only allowed on inactive plugins.
		if ( $is_active ) {
			throw new \Exception( __( 'Cannot delete an active plugin. Deactivate it first.', 'lolacore' ) );
		}
		if ( ! function_exists( 'delete_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$result = delete_plugins( array( $plugin_file ) );
		if ( is_wp_error( $result ) ) {
			throw new \Exception( esc_html( $result->get_error_message() ) );
		}
		/* translators: 1: value */
		return array( 'success' => true, 'message' => sprintf( __( "Plugin '%s' deleted.", 'lolacore' ), $plugin_name ) );
	}

	private function updatePlugin( array $params, bool $dryRun ): array {
		$slug = (string) ( $params['slug'] ?? $params['plugin'] ?? '' );
		if ( $slug === '' ) {
			throw new \Exception( __( 'Plugin slug is required.', 'lolacore' ) );
		}

		$this->ensurePluginApiLoaded();
		$all_plugins = get_plugins();
		$plugin_file = null;
		foreach ( $all_plugins as $file => $data ) {
			if ( dirname( $file ) === $slug || $file === $slug ) {
				$plugin_file = $file;
				break;
			}
		}
		if ( ! $plugin_file ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( "Plugin with slug '%s' not found.", 'lolacore' ), $slug ) );
		}

		$updates    = get_site_transient( 'update_plugins' );
		$has_update = isset( $updates->response[ $plugin_file ] );

		if ( ! $has_update ) {
			/* translators: 1: value */
			return array( 'success' => false, 'message' => sprintf( __( "Plugin '%s' is already up to date. No update available.", 'lolacore' ), $slug ) );
		}

		$new_version = $updates->response[ $plugin_file ]->new_version ?? 'unknown';

		if ( $dryRun ) {
			return array(
				'action'      => 'Will update plugin',
				'plugin'      => $slug,
				'current'     => $all_plugins[ $plugin_file ]['Version'] ?? 'unknown',
				'new_version' => $new_version,
				'warning'     => 'Ensure you have a backup before updating plugins.',
			);
		}

		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$clear_cache = $params['clear_update_cache'] ?? true;
		$skin        = new \Automatic_Upgrader_Skin();
		$upgrader    = new \Plugin_Upgrader( $skin );
		$result      = $upgrader->upgrade( $plugin_file, array( 'clear_update_cache' => $clear_cache ) );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( 'Update failed: %s', 'lolacore' ), $result->get_error_message() ) );
		}
		if ( ! $result ) {
			$error = $skin->result;
			if ( is_wp_error( $error ) ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				/* translators: 1: value */
				throw new \Exception( sprintf( __( 'Update failed: %s', 'lolacore' ), $error->get_error_message() ) );
			}
			throw new \Exception( __( 'Update failed for unknown reason. Check file permissions and ensure the filesystem is accessible.', 'lolacore' ) );
		}

		wp_clean_plugins_cache( $clear_cache );
		$refreshed_plugins = get_plugins();
		$actual_version    = $refreshed_plugins[ $plugin_file ]['Version'] ?? 'unknown';
		$previous_version  = $all_plugins[ $plugin_file ]['Version'] ?? 'unknown';

		if ( $actual_version === $previous_version ) {
			throw new \Exception(
				esc_html(
					sprintf(
						/* translators: %s: plugin version number */
						__( 'Update appeared to succeed but the plugin version did not change (still %s). Check file permissions and disk space.', 'lolacore' ),
						$actual_version
					)
				)
			);
		}

		return array(
			'success'          => true,
			'message'          => sprintf( __( "Plugin '%1\$s' updated from %2\$s to %3\$s.", 'lolacore' ), $slug, $previous_version, $actual_version ),
			'plugin'           => $slug,
			'previous_version' => $previous_version,
			'new_version'      => $actual_version,
		);
	}

	private function searchPlugins( array $params ): array {
		$keyword = $params['keyword'] ?? '';
		$limit   = min( (int) ( $params['limit'] ?? 5 ), 10 );

		if ( empty( $keyword ) ) {
			throw new \Exception( __( 'A search keyword is required.', 'lolacore' ) );
		}

		if ( ! function_exists( 'plugins_api' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		}

		$response = plugins_api( 'query_plugins', array(
			'search'   => $keyword,
			'per_page' => $limit,
			'fields'   => array(
				'short_description' => true,
				'active_installs'   => true,
				'rating'            => true,
				'num_ratings'       => true,
				'tested'            => true,
				'requires'          => true,
				'requires_php'      => true,
				'download_link'     => false,
				'sections'          => false,
				'tags'              => false,
			),
		) );

		if ( is_wp_error( $response ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( 'WordPress.org API error: %s', 'lolacore' ), $response->get_error_message() ) );
		}

		$results = array();
		if ( ! empty( $response->plugins ) ) {
			foreach ( $response->plugins as $plugin ) {
				$is_arr = is_array( $plugin );
				$get    = function ( $key ) use ( $plugin, $is_arr ) {
					return $is_arr ? ( $plugin[ $key ] ?? '' ) : ( $plugin->$key ?? '' );
				};

				$results[] = array(
					'name'              => $get( 'name' ),
					'slug'              => $get( 'slug' ),
					'version'           => $get( 'version' ),
					'author'            => wp_strip_all_tags( $get( 'author' ) ),
					'short_description' => $get( 'short_description' ),
					'active_installs'   => (int) $get( 'active_installs' ),
					'rating'            => ( (float) $get( 'rating' ) ) / 20,
					'num_ratings'       => (int) $get( 'num_ratings' ),
					'tested'            => $get( 'tested' ),
					'requires_php'      => $get( 'requires_php' ),
				);
			}
		}

		return array(
			'keyword'     => $keyword,
			'total_found' => $response->info['results'] ?? count( $results ),
			'showing'     => count( $results ),
			'plugins'     => $results,
		);
	}

	private function getPluginVersions( array $params ): array {
		$slug = $params['slug'] ?? '';
		if ( empty( $slug ) ) {
			throw new \Exception( __( 'Plugin slug is required.', 'lolacore' ) );
		}

		$limit = min( max( (int) ( $params['limit'] ?? 25 ), 1 ), 50 );

		$this->ensurePluginApiLoaded();
		$resolved = $this->resolveInstalledPlugin( $slug );
		if ( null === $resolved ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( "Plugin with slug '%s' not found.", 'lolacore' ), $slug ) );
		}

		$plugin_file = $resolved['file'];
		$current     = $resolved['version'];
		$api         = $this->fetchWpOrgPluginInfo( $slug );
		$version_map = $this->parseWpOrgVersions( $api );
		$sorted      = $this->sortVersionKeys( array_keys( $version_map ) );
		$current_idx = array_search( $current, $sorted, true );

		$rollback_candidates = array();
		if ( false !== $current_idx ) {
			$older_versions = array_slice( $sorted, $current_idx + 1 );
			foreach ( $older_versions as $offset => $version ) {
				$rollback_candidates[] = array(
					'version'    => $version,
					'steps_back' => $offset + 1,
				);
			}
		} else {
			foreach ( $sorted as $version ) {
				if ( version_compare( $version, $current, '<' ) ) {
					$rollback_candidates[] = array(
						'version'    => $version,
						'steps_back' => null,
					);
				}
			}
		}

		$showing = array_slice( $rollback_candidates, 0, $limit );

		return array(
			'slug'                     => $slug,
			'name'                     => $resolved['name'],
			'current_version'          => $current,
			'is_active'                => is_plugin_active( $plugin_file ),
			'latest_wporg_version'     => $sorted[0] ?? $current,
			'current_in_wporg_archive' => false !== $current_idx,
			'total_wporg_versions'     => count( $sorted ),
			'older_versions_available' => count( $rollback_candidates ),
			'showing'                  => count( $showing ),
			'rollback_candidates'      => $showing,
		);
	}

	private function installPlugin( array $params, bool $dryRun ): array {
		$slug = $params['slug'] ?? '';
		if ( empty( $slug ) ) {
			throw new \Exception( __( 'Plugin slug is required for installation.', 'lolacore' ) );
		}

		if ( $dryRun ) {
			return array(
				'action'      => 'Will install plugin',
				'plugin'      => $slug,
				'warning'     => 'Ensure you trust the plugin source before installing.',
			);
		}

		if ( ! function_exists( 'plugins_api' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		}
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$api = plugins_api( 'plugin_information', array(
			'slug'   => $slug,
			'fields' => array( 'sections' => false ),
		) );

		if ( is_wp_error( $api ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( 'Failed to retrieve plugin information: %s', 'lolacore' ), $api->get_error_message() ) );
		}

		$skin     = new \Automatic_Upgrader_Skin();
		$upgrader = new \Plugin_Upgrader( $skin );
		$result   = $upgrader->install( $api->download_link );

		if ( is_wp_error( $result ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( 'Installation failed: %s', 'lolacore' ), $result->get_error_message() ) );
		}
		if ( ! $result ) {
			$error = $skin->result;
			if ( is_wp_error( $error ) ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				/* translators: 1: value */
				throw new \Exception( sprintf( __( 'Installation failed: %s', 'lolacore' ), $error->get_error_message() ) );
			}
			throw new \Exception( __( 'Installation failed for unknown reason. Check file permissions and ensure the filesystem is accessible.', 'lolacore' ) );
		}

		wp_clean_plugins_cache();

		return array(
			'success' => true,
			/* translators: 1: value */
			'message' => sprintf( __( "Plugin '%s' installed successfully. It is currently inactive. You can activate it using toggle_plugin.", 'lolacore' ), $api->name ),
			'plugin'  => $slug,
		);
	}

	private function bulkUpdatePlugins( array $params, bool $dryRun ): array {
		$slugs = $params['slugs'] ?? array();
		if ( empty( $slugs ) || ! is_array( $slugs ) ) {
			throw new \Exception( __( 'An array of plugin slugs is required.', 'lolacore' ) );
		}
		if ( $dryRun ) {
			return array(
				'action' => 'Will update multiple plugins',
				'count'  => count( $slugs ),
				'slugs'  => $slugs,
			);
		}

		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		require_once ABSPATH . 'wp-admin/includes/file.php';

		$skin     = new \Automatic_Upgrader_Skin();
		$upgrader = new \Plugin_Upgrader( $skin );
		
		$files = array();
		$all_plugins = get_plugins();
		foreach ( $slugs as $slug ) {
			foreach ( $all_plugins as $file => $data ) {
				if ( dirname( $file ) === $slug || $file === $slug ) {
					$files[] = $file;
					break;
				}
			}
		}
		
		if ( empty( $files ) ) {
			throw new \Exception( __( 'None of the provided slugs matched installed plugins.', 'lolacore' ) );
		}

		$result = $upgrader->bulk_upgrade( $files );

		if ( empty( $result ) ) {
			throw new \Exception( __( 'Bulk update failed: check filesystem permissions.', 'lolacore' ) );
		}
		wp_clean_plugins_cache();

		return array(
			'success' => true,
			/* translators: 1: number */
			'message' => sprintf( __( '%d plugin(s) updated.', 'lolacore' ), count( $result ) ),
			'results' => $result,
		);
	}

	private function rollbackPlugin( array $params, bool $dryRun ): array {
		$slug = $params['slug'] ?? '';
		if ( empty( $slug ) ) {
			throw new \Exception( __( 'Plugin slug is required.', 'lolacore' ) );
		}

		if ( $this->isProtectedRollbackSlug( $slug ) ) {
			throw new \Exception( __( 'I cannot roll back LolaCore or its add-ons from here — that could break this session. Do it manually from Plugins if you really need to.', 'lolacore' ) );
		}

		$this->ensurePluginApiLoaded();
		$resolved = $this->resolveInstalledPlugin( $slug );
		if ( null === $resolved ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( "Plugin with slug '%s' not found.", 'lolacore' ), $slug ) );
		}

		$plugin_file   = $resolved['file'];
		$plugin_name   = $resolved['name'];
		$current       = $resolved['version'];
		$api           = $this->fetchWpOrgPluginInfo( $slug );
		$version_urls  = $this->parseWpOrgVersions( $api );
		$target        = $this->resolveRollbackTarget( $current, $version_urls, $params );
		$download_url  = $version_urls[ $target ] ?? $this->buildWpOrgDownloadUrl( $slug, $target );

		if ( version_compare( $target, $current, '>=' ) ) {
			throw new \Exception(
				sprintf(
					/* translators: 1: plugin slug, 2: current version, 3: target version */
					__( "Plugin '%1\$s' is already at %2\$s. Version %3\$s is not older — use update_plugin to go forward.", 'lolacore' ),
					$slug,
					$current,
					$target
				)
			);
		}

		$was_active    = is_plugin_active( $plugin_file );
		$was_network   = is_multisite() && is_plugin_active_for_network( $plugin_file );

		if ( $dryRun ) {
			return array(
				'plugin_name'     => $plugin_name,
				'current_version' => $current,
				'target_version'  => $target,
				'was_active'      => $was_active,
			);
		}

		require_once ABSPATH . 'wp-admin/includes/file.php';
		require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';

		$package = download_url( $download_url );
		if ( is_wp_error( $package ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( 'Download failed: %s', 'lolacore' ), $package->get_error_message() ) );
		}

		if ( ! wp_zip_file_is_valid( $package ) ) {
			wp_delete_file( $package );
			throw new \Exception( __( 'The downloaded package is not a valid ZIP file.', 'lolacore' ) );
		}

		if ( $was_active ) {
			deactivate_plugins( $plugin_file, true, $was_network );
		}

		$plugin_dir = WP_PLUGIN_DIR . '/' . $this->getPluginDirectoryFromFile( $plugin_file, $slug );
		if ( is_dir( $plugin_dir ) ) {
			$this->deletePluginDirectory( $plugin_dir );
		}

		// phpcs:ignore PluginCheck.CodeAnalysis.WriteFile.PluginDirectoryWrite -- Rollback replaces plugin files under WP_PLUGIN_DIR, same as core Plugin_Upgrader.
		$unzip = unzip_file( $package, WP_PLUGIN_DIR );
		wp_delete_file( $package );

		if ( is_wp_error( $unzip ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: value */
			throw new \Exception( sprintf( __( 'Rollback failed while unpacking: %s', 'lolacore' ), $unzip->get_error_message() ) );
		}

		wp_clean_plugins_cache( true );

		$refreshed     = get_plugins();
		$resolved_after = $this->resolveInstalledPlugin( $slug );
		$new_file      = $resolved_after['file'] ?? $plugin_file;
		$actual        = $refreshed[ $new_file ]['Version'] ?? $target;
		$reactivated   = false;
		$activate_note = '';

		if ( $was_active && ! wp_doing_cron() ) {
			$activate_result = activate_plugin( $new_file, '', $was_network, true );
			if ( is_wp_error( $activate_result ) ) {
				$activate_note = sprintf(
					/* translators: 1: error message */
					__( ' Files were replaced but reactivation failed: %s', 'lolacore' ),
					$activate_result->get_error_message()
				);
			} else {
				$reactivated = true;
			}
		}

		return array(
			'success'          => true,
			'message'          => sprintf(
				/* translators: 1: plugin slug, 2: previous version, 3: new version */
				__( "Plugin '%1\$s' rolled back from %2\$s to %3\$s.%4\$s", 'lolacore' ),
				$slug,
				$current,
				$actual,
				$activate_note
			),
			'plugin'           => $slug,
			'previous_version' => $current,
			'new_version'      => $actual,
			'target_version'   => $target,
			'reactivated'      => $reactivated,
		);
	}

	/**
	 * @return array{file: string, name: string, version: string}|null
	 */
	private function resolveInstalledPlugin( string $slug ): ?array {
		$all_plugins = get_plugins();
		foreach ( $all_plugins as $file => $data ) {
			if ( dirname( $file ) === $slug || $file === $slug ) {
				return array(
					'file'    => $file,
					'name'    => $data['Name'] ?? $slug,
					'version' => $data['Version'] ?? 'unknown',
				);
			}
		}
		return null;
	}

	private function isProtectedRollbackSlug( string $slug ): bool {
		if ( 'lolacore' === $slug ) {
			return true;
		}
		return str_starts_with( $slug, 'lolacore-' );
	}

	private function fetchWpOrgPluginInfo( string $slug ): object {
		if ( ! function_exists( 'plugins_api' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		}

		$api = plugins_api(
			'plugin_information',
			array(
				'slug'   => $slug,
				'fields' => array(
					'sections' => false,
					'versions' => true,
				),
			)
		);

		if ( is_wp_error( $api ) || empty( $api ) || isset( $api->error ) ) {
			// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
			/* translators: 1: plugin slug */
			throw new \Exception(
				sprintf(
					__( "I can't find '%1\$s' in the WordPress.org plugin directory. Version history and rollback are only available for plugins hosted on WordPress.org.", 'lolacore' ),
					$slug
				)
			);
		}

		return $api;
	}

	/**
	 * @return array<string, string> version => download URL
	 */
	private function parseWpOrgVersions( object $api ): array {
		$versions = array();
		if ( ! isset( $api->versions ) ) {
			return $versions;
		}

		foreach ( (array) $api->versions as $version => $url ) {
			if ( 'trunk' === $version ) {
				continue;
			}
			if ( ! is_string( $version ) || ! preg_match( '/^\d+(\.\d+)*(-[a-zA-Z0-9]+(\.[a-zA-Z0-9]+)*)?$/', $version ) ) {
				continue;
			}
			if ( is_string( $url ) && $url !== '' ) {
				$versions[ $version ] = $url;
			}
		}

		if ( empty( $versions ) ) {
			throw new \Exception( __( 'WordPress.org returned no downloadable versions for this plugin.', 'lolacore' ) );
		}

		return $versions;
	}

	/**
	 * @param array<string, string> $versions
	 */
	private function resolveRollbackTarget( string $current, array $versions, array $params ): string {
		$target_version = isset( $params['target_version'] ) ? trim( (string) $params['target_version'] ) : '';
		$steps_back     = isset( $params['steps_back'] ) ? (int) $params['steps_back'] : 0;

		if ( $target_version !== '' && $steps_back > 0 ) {
			throw new \Exception( __( 'Use either target_version or steps_back, not both.', 'lolacore' ) );
		}

		if ( $target_version !== '' ) {
			if ( ! isset( $versions[ $target_version ] ) ) {
				// phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
				/* translators: 1: version number */
				throw new \Exception( sprintf( __( "Version '%s' is not available on WordPress.org for this plugin.", 'lolacore' ), $target_version ) );
			}
			return $target_version;
		}

		if ( $steps_back < 1 ) {
			$steps_back = 1;
		}

		$sorted = $this->sortVersionKeys( array_keys( $versions ) );
		$index  = array_search( $current, $sorted, true );
		if ( false === $index ) {
			throw new \Exception( __( 'Your installed version is not in the WordPress.org archive. Tell me the exact version number to roll back to.', 'lolacore' ) );
		}

		$target_index = $index + $steps_back;
		if ( ! isset( $sorted[ $target_index ] ) ) {
			$older_count = max( 0, count( $sorted ) - $index - 1 );
			throw new \Exception(
				sprintf(
					/* translators: 1: number of older versions available */
					__( 'Only %d older version(s) exist on WordPress.org. Pick a closer target.', 'lolacore' ),
					$older_count
				)
			);
		}

		return $sorted[ $target_index ];
	}

	/**
	 * @param array<int, string> $keys
	 * @return array<int, string>
	 */
	private function sortVersionKeys( array $keys ): array {
		usort(
			$keys,
			static function ( string $a, string $b ): int {
				return version_compare( $b, $a );
			}
		);
		return $keys;
	}

	private function buildWpOrgDownloadUrl( string $slug, string $version ): string {
		return sprintf( 'https://downloads.wordpress.org/plugin/%s.%s.zip', rawurlencode( $slug ), rawurlencode( $version ) );
	}

	private function getPluginDirectoryFromFile( string $plugin_file, string $slug ): string {
		$dir = dirname( $plugin_file );
		if ( '.' === $dir ) {
			return basename( $plugin_file, '.php' );
		}
		return $dir;
	}

	private function deletePluginDirectory( string $plugin_dir ): void {
		require_once ABSPATH . 'wp-admin/includes/file.php';

		if ( ! \WP_Filesystem() ) {
			throw new \Exception( __( 'I could not access the filesystem. Check that WordPress can write to the plugins folder.', 'lolacore' ) );
		}

		global $wp_filesystem;
		// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound
		if ( $wp_filesystem->is_dir( $plugin_dir ) && ! $wp_filesystem->delete( $plugin_dir, true ) ) {
			throw new \Exception( __( 'I could not remove the current plugin files before rollback.', 'lolacore' ) );
		}
	}
}

