<?php
/**
 * Install Bundled Plugin — server-side ability used by the Laravel-driven
 * website builder (SiteDependencyService::installPlugin) in place of
 * `wp plugin install <slug> --activate` and `wp plugin activate <slug>`,
 * both of which are now refused by RunWpCli's dispatcher and redirected to
 * the browser-proxied `zipai/install-plugin` / `zipai/activate-plugin`
 * js_hook tools — those can't be used from a Laravel server-to-server
 * call because there is no browser session.
 *
 * Runs `Plugin_Upgrader` server-side, the same path admin's "Install Now"
 * uses. Server-side install is only acceptable here because:
 *   1. The caller is the trusted Laravel builder backend (Bearer-auth).
 *   2. The acting WP user (resolved by `setup_user_context()`) must have
 *      `install_plugins` + `activate_plugins`.
 *   3. The slug is constrained to lower-case alphanumerics + hyphens
 *      (WP.org plugin-directory naming rules).
 *   4. `DISALLOW_FILE_MODS` is honored.
 *
 * The ability is hidden from the LLM catalog (`meta.visibility = internal`)
 * so chat-driven installs continue to flow through the browser-proxied path.
 *
 * @since 0.0.5
 * @package zip-ai
 */

namespace ZipAI\MCP\Classes\Abilities\Zipai\Builder;

use ZipAI\MCP\Classes\Abilities\Abstract_Ability;
use ZipAI\MCP\Classes\Core\Tool_Types;
use ZipAI\MCP\Classes\Core\Response;
use ZipAI\MCP\Classes\Core\Plugin_Abilities_Toggler;

defined( 'ABSPATH' ) || exit;

/**
 * Ability: install (if missing) and activate a wp.org plugin slug.
 */
class InstallBundledPlugin extends Abstract_Ability {

	protected $is_destructive = true;

	public function configure() {
		$this->id          = 'zipai/install-bundled-plugin';
		$this->label       = 'Install Bundled Plugin (server-side)';
		$this->description = 'Install and optionally activate a wp.org plugin slug. Internal builder-side replacement for `wp plugin install <slug> --activate` / `wp plugin activate <slug>` — those wp-cli paths route to browser-proxied js_hook tools which cannot run from a Laravel server-to-server call.';
		// Caller must hold BOTH caps. `install_plugins` is checked again
		// inside `Plugin_Upgrader::install()` by core; we re-check here so
		// any future direct caller still gets the same gate.
		$this->capability  = 'install_plugins';

		$this->meta['visibility'] = 'internal';
	}

	public function get_tool_type() {
		return Tool_Types::ACTION;
	}

	public function get_input_schema() {
		return array(
			'type'                 => 'object',
			'required'             => array( 'slug' ),
			'additionalProperties' => false,
			'properties'           => array(
				'slug'     => array(
					'type'        => 'string',
					'description' => 'wp.org plugin slug (lower-case alphanumerics and hyphens, e.g. "contact-form-7"). NOT a `folder/file.php` shape.',
					'pattern'     => '^[a-z0-9][a-z0-9-]{0,62}$',
				),
				'activate' => array(
					'type'        => 'boolean',
					'description' => 'Activate after install. Default true. If the plugin is already installed but inactive, activation still runs.',
					'default'     => true,
				),
			),
		);
	}

	public function execute( $args ) {
		// Validate the RAW input against the slug regex first — `sanitize_key`
		// silently strips slashes / dots / spaces which would otherwise let
		// shapes like "foo/bar.php" pass and then fail later at wp.org lookup
		// with a misleading "Plugin not found" message.
		$raw_slug = isset( $args['slug'] ) ? (string) $args['slug'] : '';
		$activate = isset( $args['activate'] ) ? (bool) $args['activate'] : true;

		if ( '' === $raw_slug || ! preg_match( '/^[a-z0-9][a-z0-9-]{0,62}$/', $raw_slug ) ) {
			return self::coded_error( 'Invalid slug — expected wp.org plugin slug (a–z, 0–9, hyphens).', 'invalid_slug' );
		}
		$slug = sanitize_key( $raw_slug );

		// File-mods gate first — if file mods are off, capability state is moot.
		if ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ) {
			return self::coded_error( 'File modifications are disabled on this site (DISALLOW_FILE_MODS).', 'file_mods_disabled' );
		}

		// Capability double-check — defense-in-depth on top of the Abilities
		// API `permission_callback`.
		if ( ! current_user_can( 'install_plugins' ) ) {
			return self::coded_error( 'You do not have install_plugins capability.', 'insufficient_capability' );
		}
		if ( $activate && ! current_user_can( 'activate_plugins' ) ) {
			return self::coded_error( 'You do not have activate_plugins capability.', 'insufficient_capability' );
		}

		// Network-admin gate — installing into a multisite from a sub-site is
		// surprising; restrict to network admins.
		if ( is_multisite() && ! is_super_admin() ) {
			return self::coded_error( 'On multisite, only network super-admins may install plugins.', 'multisite_super_admin_required' );
		}

		// Bring in admin-only loaders.
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		if ( ! function_exists( 'plugins_api' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
		}
		if ( ! class_exists( 'Plugin_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}
		if ( ! class_exists( 'WP_Ajax_Upgrader_Skin' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
		}
		// Plugin_Upgrader may call request_filesystem_credentials() through
		// WP_Upgrader internals. Load file.php explicitly in non-admin
		// contexts to avoid fatal "undefined function" during server-side
		// installs invoked by Laravel.
		if ( ! function_exists( 'request_filesystem_credentials' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		// Fast path: already installed → skip install, jump to activate.
		$existing_file = $this->find_plugin_file_for_slug( $slug );

		if ( null === $existing_file ) {
			// Not installed — fetch package URL + run install.
			$information = plugins_api(
				'plugin_information',
				array(
					'slug'   => $slug,
					'fields' => array( 'sections' => false ),
				)
			);
			if ( is_wp_error( $information ) ) {
				return self::coded_error( 'wp.org lookup failed: ' . $information->get_error_message(), 'wporg_lookup_failed' );
			}
			$package = is_object( $information ) && isset( $information->download_link ) ? (string) $information->download_link : '';
			if ( '' === $package ) {
				return self::coded_error( sprintf( 'wp.org returned no download link for "%s".', $slug ), 'wporg_missing_download_link' );
			}

			$upgrader = new \Plugin_Upgrader( new \WP_Ajax_Upgrader_Skin() );
			$result   = $upgrader->install( $package );
			if ( is_wp_error( $result ) ) {
				return self::coded_error( 'install failed: ' . $result->get_error_message(), 'install_failed' );
			}
			if ( ! $result ) {
				return self::coded_error( 'install failed (unknown error).', 'install_failed' );
			}
			$existing_file = $upgrader->plugin_info();
			if ( ! $existing_file ) {
				$existing_file = $this->find_plugin_file_for_slug( $slug );
			}
			if ( ! $existing_file ) {
				return self::coded_error( 'install succeeded but plugin file could not be resolved.', 'plugin_file_unresolved' );
			}
		}

		$was_active = is_plugin_active( $existing_file ) || is_plugin_active_for_network( $existing_file );
		if ( $activate && ! $was_active ) {
			$activation = activate_plugin( $existing_file );
			if ( is_wp_error( $activation ) ) {
				return self::coded_error( 'activation failed: ' . $activation->get_error_message(), 'activation_failed' );
			}
			// `activate_plugin()` returns null on success OR when an
			// `update_option(active_plugins)` filter silently rejected the
			// write. Verify post-state so silent-failure surfaces as a real
			// error instead of a false success.
			if ( ! is_plugin_active( $existing_file ) && ! is_plugin_active_for_network( $existing_file ) ) {
				return self::coded_error(
					sprintf( 'activate_plugin() returned no error for "%s" but plugin is still inactive — likely an `update_option(active_plugins)` filter rejected the write.', $slug ),
					'activation_rejected'
				);
			}
		}

		$toggle_result = Plugin_Abilities_Toggler::enable_for_slug( $slug );
		return Response::success(
			array(
				'slug'     => $slug,
				'file'     => $existing_file,
				'active'   => $activate ? ( is_plugin_active( $existing_file ) || is_plugin_active_for_network( $existing_file ) ) : $was_active,
				'toggles'  => $toggle_result,
				'message'  => $activate
					? sprintf( 'Plugin "%s" installed and active.', $slug )
					: sprintf( 'Plugin "%s" installed.', $slug ),
			)
		);
	}

	/**
	 * Resolve a plugin slug to the `folder/file.php` shape from get_plugins().
	 * Returns null when the slug isn't installed.
	 *
	 * @param string $slug wp.org slug.
	 * @return string|null
	 */
	private function find_plugin_file_for_slug( string $slug ): ?string {
		$plugins = get_plugins();
		$slug_lc = strtolower( $slug );
		foreach ( array_keys( $plugins ) as $file ) {
			$folder = strtolower( explode( '/', $file )[0] );
			if ( $folder === $slug_lc ) {
				return $file;
			}
		}
		return null;
	}

	public function get_output_schema() {
		return array(
			'type'       => 'object',
			'properties' => array(
				'success' => array( 'type' => 'boolean' ),
				'message' => array( 'type' => 'object' ),
			),
		);
	}

	/**
	 * Error payload with machine-readable code for callers.
	 *
	 * @param string $message Human-readable error.
	 * @param string $code    Stable error code.
	 * @return array<string,mixed>
	 */
	private static function coded_error( string $message, string $code ): array {
		return array(
			'success'    => false,
			'error'      => $message,
			'error_code' => $code,
		);
	}
}
