<?php

declare( strict_types=1 );

namespace StrykeWP\Modules\Connector;

/**
 * API Manager — Remote Action & Maintenance Engine for Hub Connector.
 *
 * Ported from StrykeHub_API_Manager (stryke-connector v1.0.0).
 * Handles all REST write/action endpoints (plugins/manage, system/maintenance).
 *
 * @package StrykeWP\Modules\Connector
 */
class ApiManager {

	// -----------------------------------------------------------------------
	// Plugin Manage: Arg Validators & Sanitizers
	// -----------------------------------------------------------------------

	/**
	 * Validates the `plugin_file` REST argument (format checks only, no I/O).
	 *
	 * @param mixed            $value   Raw value from the request body.
	 * @param \WP_REST_Request $request The full request object (unused).
	 * @param string           $param   The parameter name (unused).
	 * @return bool|\WP_Error
	 */
	public function validate_plugin_file( mixed $value, \WP_REST_Request $request, string $param ): bool|\WP_Error {

		if ( ! is_string( $value ) || '' === trim( $value ) ) {
			return new \WP_Error( 'stryke_invalid_plugin_file', __( 'plugin_file must be a non-empty string.', 'stryke-wp' ), array( 'status' => 400 ) );
		}

		if ( ! str_ends_with( $value, '.php' ) ) {
			return new \WP_Error( 'stryke_invalid_plugin_file', __( 'plugin_file must end with .php (e.g. "akismet/akismet.php").', 'stryke-wp' ), array( 'status' => 400 ) );
		}

		if ( str_contains( $value, '..' ) ) {
			return new \WP_Error( 'stryke_invalid_plugin_file', __( 'plugin_file must not contain path traversal sequences.', 'stryke-wp' ), array( 'status' => 400 ) );
		}

		if ( str_starts_with( $value, '/' ) || str_starts_with( $value, '\\' ) ) {
			return new \WP_Error( 'stryke_invalid_plugin_file', __( 'plugin_file must be a relative path, not an absolute one.', 'stryke-wp' ), array( 'status' => 400 ) );
		}

		if ( str_contains( $value, "\0" ) ) {
			return new \WP_Error( 'stryke_invalid_plugin_file', __( 'plugin_file must not contain null bytes.', 'stryke-wp' ), array( 'status' => 400 ) );
		}

		return true;
	}

	/**
	 * Sanitizes the `plugin_file` REST argument.
	 * Normalizes separators and strips invalid characters.
	 *
	 * @param mixed $value Raw value from the request body.
	 * @return string Sanitized plugin file identifier.
	 */
	public function sanitize_plugin_file( mixed $value ): string {
		$value = str_replace( '\\', '/', (string) $value );
		return preg_replace( '/[^a-zA-Z0-9.\-_\/]/', '', $value );
	}

	// -----------------------------------------------------------------------
	// Plugin Manage: Main Callback
	// -----------------------------------------------------------------------

	/**
	 * Endpoint: POST /stryke/v1/plugins/manage
	 *
	 * @param \WP_REST_Request $request Incoming POST request.
	 * @return \WP_REST_Response
	 */
	public function handle_plugin_manage( \WP_REST_Request $request ): \WP_REST_Response {

		$this->require_plugin_functions();

		$plugin_file = $request->get_param( 'plugin_file' );
		$action      = $request->get_param( 'action' );

		// Guard 1: Self-protection — cannot manage the STRYKE WP plugin itself.
		if ( $plugin_file === STRYKE_WP_BASENAME ) {
			return $this->error_response(
				__(
					'Security violation: the STRYKE WP plugin cannot modify itself. '
					. 'Use the WordPress admin to manage this plugin directly.',
					'stryke-wp'
				),
				null,
				403
			);
		}

		// Guard 2: Existence check.
		$all_plugins = get_plugins();

		if ( ! array_key_exists( $plugin_file, $all_plugins ) ) {
			return $this->error_response(
				sprintf(
					/* translators: %s = plugin_file value from the request */
					__( 'Plugin not found: "%s". Confirm the plugin_file value from GET /plugins.', 'stryke-wp' ),
					$plugin_file
				),
				array( 'plugin_file' => $plugin_file ),
				404
			);
		}

		$plugin_data = $all_plugins[ $plugin_file ];

		return match ( $action ) {
			'activate'   => $this->action_activate_plugin( $plugin_file, $plugin_data ),
			'deactivate' => $this->action_deactivate_plugin( $plugin_file, $plugin_data ),
			'update'     => $this->action_update_plugin( $plugin_file, $plugin_data ),
			default      => $this->error_response(
				__( 'Unknown action. Accepted values: activate, deactivate, update.', 'stryke-wp' ),
				null,
				400
			),
		};
	}

	// -----------------------------------------------------------------------
	// Action Dispatchers
	// -----------------------------------------------------------------------

	/**
	 * Activates a plugin. Returns 409 Conflict if already active.
	 *
	 * @param string $plugin_file Plugin identifier.
	 * @param array  $plugin_data Plugin header data.
	 * @return \WP_REST_Response
	 */
	private function action_activate_plugin( string $plugin_file, array $plugin_data ): \WP_REST_Response {

		$already_network_active = is_multisite() && is_plugin_active_for_network( $plugin_file );

		if ( is_plugin_active( $plugin_file ) || $already_network_active ) {
			return $this->error_response(
				sprintf( __( 'Plugin "%s" is already active.', 'stryke-wp' ), $plugin_data['Name'] ),
				array(
					'plugin_file' => $plugin_file,
					'status'      => $already_network_active ? 'network-active' : 'active',
				),
				409
			);
		}

		$result = activate_plugin( $plugin_file, '', is_multisite() && is_network_admin() );

		if ( is_wp_error( $result ) ) {
			return $this->error_response(
				$result->get_error_message(),
				array( 'plugin_file' => $plugin_file, 'wp_error_code' => $result->get_error_code() ),
				500
			);
		}

		return $this->success_response(
			sprintf( __( 'Plugin "%s" activated successfully.', 'stryke-wp' ), $plugin_data['Name'] ),
			array(
				'plugin_file' => $plugin_file,
				'name'        => sanitize_text_field( $plugin_data['Name'] ),
				'version'     => sanitize_text_field( $plugin_data['Version'] ),
				'status'      => 'active',
			)
		);
	}

	/**
	 * Deactivates a plugin. Returns 409 Conflict if already inactive.
	 *
	 * @param string $plugin_file Plugin identifier.
	 * @param array  $plugin_data Plugin header data.
	 * @return \WP_REST_Response
	 */
	private function action_deactivate_plugin( string $plugin_file, array $plugin_data ): \WP_REST_Response {

		$is_network_active = is_multisite() && is_plugin_active_for_network( $plugin_file );
		$is_site_active    = is_plugin_active( $plugin_file );

		if ( ! $is_site_active && ! $is_network_active ) {
			return $this->error_response(
				sprintf( __( 'Plugin "%s" is already inactive.', 'stryke-wp' ), $plugin_data['Name'] ),
				array( 'plugin_file' => $plugin_file, 'status' => 'inactive' ),
				409
			);
		}

		deactivate_plugins( $plugin_file, false, $is_network_active );

		if ( is_plugin_active( $plugin_file ) || ( is_multisite() && is_plugin_active_for_network( $plugin_file ) ) ) {
			return $this->error_response(
				sprintf( __( 'Failed to deactivate plugin "%s". Permissions may prevent this.', 'stryke-wp' ), $plugin_data['Name'] ),
				array( 'plugin_file' => $plugin_file ),
				500
			);
		}

		return $this->success_response(
			sprintf( __( 'Plugin "%s" deactivated successfully.', 'stryke-wp' ), $plugin_data['Name'] ),
			array(
				'plugin_file' => $plugin_file,
				'name'        => sanitize_text_field( $plugin_data['Name'] ),
				'version'     => sanitize_text_field( $plugin_data['Version'] ),
				'status'      => 'inactive',
			)
		);
	}

	/**
	 * Updates a plugin via Plugin_Upgrader with a silent skin.
	 *
	 * @param string $plugin_file Plugin identifier.
	 * @param array  $plugin_data Plugin header data (pre-update).
	 * @return \WP_REST_Response
	 */
	private function action_update_plugin( string $plugin_file, array $plugin_data ): \WP_REST_Response {

		if ( defined( 'DISALLOW_FILE_MODS' ) && DISALLOW_FILE_MODS ) {
			return $this->error_response(
				__( 'File modifications are disabled on this server (DISALLOW_FILE_MODS).', 'stryke-wp' ),
				array( 'plugin_file' => $plugin_file ),
				403
			);
		}

		$previous_version = sanitize_text_field( $plugin_data['Version'] );
		$plugin_name      = sanitize_text_field( $plugin_data['Name'] );

		// Verify update availability.
		$update_transient = get_site_transient( 'update_plugins' );

		if ( ! isset( $update_transient->response[ $plugin_file ] ) ) {
			return $this->error_response(
				sprintf(
					/* translators: 1 = plugin name, 2 = current version */
					__( 'No update available for "%1$s" (current: %2$s).', 'stryke-wp' ),
					$plugin_name,
					$previous_version
				),
				array( 'plugin_file' => $plugin_file, 'current_version' => $previous_version ),
				400
			);
		}

		$target_version = sanitize_text_field(
			$update_transient->response[ $plugin_file ]->new_version ?? 'unknown'
		);

		// Load upgrader classes + silent skin.
		$this->require_upgrader_classes();

		// Initialize WP_Filesystem in direct mode.
		if ( ! $this->init_filesystem() ) {
			return $this->error_response(
				__(
					'Filesystem initialization failed. '
					. 'Ensure PHP has write access to the plugins directory, '
					. 'or set FS_METHOD=\'direct\' in wp-config.php.',
					'stryke-wp'
				),
				array( 'plugin_file' => $plugin_file ),
				500
			);
		}

		// Run the upgrade, buffered to suppress HTML output.
		$skin     = new SilentUpgraderSkin();
		$upgrader = new \Plugin_Upgrader( $skin );

		ob_start();
		$result = $upgrader->upgrade( $plugin_file );
		ob_end_clean();

		// Interpret the result.
		if ( is_wp_error( $result ) ) {
			return $this->error_response(
				$result->get_error_message(),
				array( 'plugin_file' => $plugin_file, 'target_version' => $target_version, 'wp_error_code' => $result->get_error_code() ),
				500
			);
		}

		if ( null === $result ) {
			return $this->error_response(
				__( 'Update aborted: the server requires filesystem credentials. Add define(\'FS_METHOD\', \'direct\') to wp-config.php.', 'stryke-wp' ),
				array( 'plugin_file' => $plugin_file ),
				500
			);
		}

		if ( false === $result ) {
			$skin_errors = $skin->get_error_messages();
			return $this->error_response(
				! empty( $skin_errors )
					? implode( ' ', $skin_errors )
					: __( 'Update failed: could not download or install the update package.', 'stryke-wp' ),
				array( 'plugin_file' => $plugin_file, 'target_version' => $target_version ),
				500
			);
		}

		// Clean caches and confirm installed version.
		wp_clean_plugins_cache( false );
		delete_site_transient( 'update_plugins' );

		$updated_plugins = get_plugins();
		$new_version     = isset( $updated_plugins[ $plugin_file ] )
			? sanitize_text_field( $updated_plugins[ $plugin_file ]['Version'] )
			: $target_version;

		return $this->success_response(
			sprintf(
				/* translators: 1 = plugin name, 2 = old version, 3 = new version */
				__( 'Plugin "%1$s" updated successfully from %2$s to %3$s.', 'stryke-wp' ),
				$plugin_name,
				$previous_version,
				$new_version
			),
			array(
				'plugin_file'      => $plugin_file,
				'name'             => $plugin_name,
				'previous_version' => $previous_version,
				'new_version'      => $new_version,
			)
		);
	}

	// -----------------------------------------------------------------------
	// System Maintenance: Main Callback
	// -----------------------------------------------------------------------

	/**
	 * Endpoint: POST /stryke/v1/system/maintenance
	 *
	 * @param \WP_REST_Request $request Incoming POST request.
	 * @return \WP_REST_Response
	 */
	public function handle_system_maintenance( \WP_REST_Request $request ): \WP_REST_Response {

		$action = $request->get_param( 'action' );

		return match ( $action ) {
			'flush_cache'   => $this->action_flush_cache(),
			'optimize_logs' => $this->action_optimize_logs(),
			default         => $this->error_response(
				__( 'Unknown maintenance action. Accepted values: flush_cache, optimize_logs.', 'stryke-wp' ),
				null,
				400
			),
		};
	}

	// -----------------------------------------------------------------------
	// Maintenance Action Dispatchers
	// -----------------------------------------------------------------------

	/**
	 * Flushes the object cache and invalidates update transients.
	 *
	 * @return \WP_REST_Response
	 */
	private function action_flush_cache(): \WP_REST_Response {

		$cache_flushed = wp_cache_flush();
		wp_clean_plugins_cache( false );

		$cleared_transients = array( 'update_plugins', 'update_themes', 'update_core' );
		array_map( 'delete_site_transient', $cleared_transients );

		return $this->success_response(
			__( 'Cache flushed and update transients invalidated successfully.', 'stryke-wp' ),
			array(
				'object_cache_flushed' => $cache_flushed,
				'transients_cleared'   => $cleared_transients,
			)
		);
	}

	/**
	 * Runs OPTIMIZE TABLE on the STRYKE Hub audit log table.
	 *
	 * @return \WP_REST_Response
	 */
	private function action_optimize_logs(): \WP_REST_Response {
		global $wpdb;

		$table_name = ApiLogger::get_table_name();

		// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$result = $wpdb->query( "OPTIMIZE TABLE `{$table_name}`" );

		if ( false === $result ) {
			return $this->error_response(
				sprintf( __( 'OPTIMIZE TABLE failed for "%s". Check the MySQL error log for details.', 'stryke-wp' ), $table_name ),
				array( 'table' => $table_name ),
				500
			);
		}

		return $this->success_response(
			sprintf( __( 'OPTIMIZE TABLE completed successfully for "%s".', 'stryke-wp' ), $table_name ),
			array( 'table' => $table_name )
		);
	}

	// -----------------------------------------------------------------------
	// Infrastructure Helpers
	// -----------------------------------------------------------------------

	/**
	 * Initializes WP_Filesystem in 'direct' mode for use in REST context.
	 *
	 * @return bool True if initialization succeeded.
	 */
	private function init_filesystem(): bool {
		global $wp_filesystem;

		if ( $wp_filesystem instanceof \WP_Filesystem_Base ) {
			return true;
		}

		if ( ! function_exists( 'WP_Filesystem' ) ) {
			require_once ABSPATH . 'wp-admin/includes/file.php';
		}

		add_filter( 'filesystem_method', array( $this, 'force_direct_filesystem_method' ) );
		$result = WP_Filesystem();
		remove_filter( 'filesystem_method', array( $this, 'force_direct_filesystem_method' ) );

		return (bool) $result;
	}

	/**
	 * Filter callback: forces 'direct' filesystem method.
	 * Must be public for remove_filter() to work by object reference.
	 *
	 * @return string Always 'direct'.
	 */
	public function force_direct_filesystem_method(): string {
		return 'direct';
	}

	/**
	 * Lazy-loads WP upgrader classes and the SilentUpgraderSkin.
	 * The WP_Upgrader parent must load before SilentUpgraderSkin can be autoloaded.
	 *
	 * @return void
	 */
	private function require_upgrader_classes(): void {
		if ( ! class_exists( 'WP_Upgrader' ) ) {
			require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
		}
		// SilentUpgraderSkin extends WP_Upgrader_Skin — PSR-4 autoloader resolves it now.
	}

	/**
	 * Lazy-loads WordPress admin plugin functions (not available outside admin by default).
	 *
	 * @return void
	 */
	private function require_plugin_functions(): void {
		if ( ! function_exists( 'get_plugins' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
	}

	// -----------------------------------------------------------------------
	// Response Builders
	// -----------------------------------------------------------------------

	/**
	 * Builds a standardized success JSON response.
	 *
	 * @param string     $message Success description.
	 * @param array|null $data    Optional payload.
	 * @param int        $status  HTTP status code (default 200).
	 * @return \WP_REST_Response
	 */
	private function success_response( string $message, ?array $data = null, int $status = 200 ): \WP_REST_Response {
		return new \WP_REST_Response( array( 'success' => true, 'message' => $message, 'data' => $data ), $status );
	}

	/**
	 * Builds a standardized error JSON response.
	 *
	 * @param string     $message Error description.
	 * @param array|null $data    Optional context.
	 * @param int        $status  HTTP error status code.
	 * @return \WP_REST_Response
	 */
	private function error_response( string $message, ?array $data = null, int $status = 400 ): \WP_REST_Response {
		return new \WP_REST_Response( array( 'success' => false, 'message' => $message, 'data' => $data ), $status );
	}
}
