<?php
/**
 * `wp blaminhor fer ...` — Fatal Error Recovery commands.
 *
 * These commands operate on the `active_plugins` option directly so they
 * keep working even when other modules are crashing — same design goal
 * as the recovery URL handler.
 *
 * @package BlaminhorEssentials
 */

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

if ( ! defined( 'WP_CLI' ) || ! WP_CLI ) {
	return;
}

class Blaminhor_Essentials_CLI_Fatal_Error_Recovery extends Blaminhor_Essentials_CLI_Module {

	/**
	 * @var string
	 */
	protected $module_id = 'fatal-error-recovery';

	/**
	 * Show the last recorded fatal error + the recovery URL.
	 *
	 * ## OPTIONS
	 *
	 * [--format=<format>]
	 * : Render format. Default: table.
	 *
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function status( $args, $assoc_args ) {
		$module  = $this->get_module_instance();
		$key     = (string) $module->get_setting( 'recovery_key', '' );
		$key_url = '' !== $key ? add_query_arg( 'be_recovery', $key, home_url( '/' ) ) : '';

		// Try to read .last-error.json if present (the module writes one on shutdown).
		$last_error_path = trailingslashit( WP_CONTENT_DIR ) . 'uploads/blaminhor-essentials/.last-error.json';
		$last_error      = file_exists( $last_error_path ) ? @json_decode( file_get_contents( $last_error_path ), true ) : null;
		$last_error      = is_array( $last_error ) ? $last_error : array();

		$rows = array(
			array( 'key' => 'recovery_key_set',  'value' => '' !== $key ? 'true' : 'false' ),
			array( 'key' => 'recovery_url',      'value' => $key_url ),
			array( 'key' => 'last_error_time',   'value' => isset( $last_error['time'] ) ? (string) $last_error['time'] : '' ),
			array( 'key' => 'last_error_type',   'value' => isset( $last_error['type'] ) ? (string) $last_error['type'] : '' ),
			array( 'key' => 'last_error_file',   'value' => isset( $last_error['file'] ) ? (string) $last_error['file'] : '' ),
			array( 'key' => 'last_error_line',   'value' => isset( $last_error['line'] ) ? (string) (int) $last_error['line'] : '' ),
			array( 'key' => 'last_error_message','value' => isset( $last_error['message'] ) ? (string) $last_error['message'] : '' ),
		);
		$this->format_for_output( $rows, $assoc_args, array( 'key', 'value' ) );
	}

	/**
	 * Print the recovery key (masked unless --reveal).
	 *
	 * ## OPTIONS
	 *
	 * [--reveal]
	 * : Print the key in clear. By default the key is masked.
	 *
	 * @subcommand get-recovery-key
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function get_recovery_key( $args, $assoc_args ) {
		$this->ensure_capabilities();
		$module = $this->get_module_instance();
		$key    = (string) $module->get_setting( 'recovery_key', '' );
		if ( '' === $key ) {
			WP_CLI::error( __( 'No recovery key set yet. Run `wp blaminhor fer regenerate-key`.', 'blaminhor-essentials' ) );
		}
		WP_CLI::line( isset( $assoc_args['reveal'] ) ? $key : $this->mask_sensitive( $key, 4 ) );
	}

	/**
	 * Generate a fresh recovery key (invalidates the previous URL).
	 *
	 * @subcommand regenerate-key
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args (unused).
	 * @param array $assoc_args Associative args.
	 */
	public function regenerate_key( $args, $assoc_args ) {
		$this->ensure_capabilities();
		$module      = $this->get_module_instance();
		$new_key     = blaminhor_essentials_generate_token( 32 );
		$settings    = $module->get_effective_settings();
		$settings['recovery_key'] = $new_key;

		$result = $module->apply_settings( $settings );
		if ( is_wp_error( $result ) ) {
			WP_CLI::error( $result->get_error_message() );
		}

		// Also persist the key file (the module reads it as a fallback when DB is broken).
		$upload_dir = wp_upload_dir();
		$key_file   = trailingslashit( $upload_dir['basedir'] ) . 'blaminhor-essentials/.recovery-key';
		if ( ! file_exists( dirname( $key_file ) ) ) {
			wp_mkdir_p( dirname( $key_file ) );
		}
		@file_put_contents( $key_file, $new_key, LOCK_EX );

		blaminhor_essentials_log( 'FER recovery key regenerated via WP-CLI.', 'info', 'cli', array( 'event' => 'fer_regenerate_key' ) );
		WP_CLI::success( __( 'Recovery key regenerated. The previous recovery URL is now invalid.', 'blaminhor-essentials' ) );
	}

	/**
	 * Force-disable a plugin by editing active_plugins directly.
	 *
	 * Bypasses the deactivation hooks — useful when a plugin is crashing
	 * during deactivation. Equivalent to clicking "disable" on the
	 * recovery page.
	 *
	 * ## OPTIONS
	 *
	 * <plugin>
	 * : Plugin slug (folder name) OR full path (folder/file.php).
	 *
	 * [--yes]
	 * : Skip the interactive confirmation prompt.
	 *
	 * @subcommand force-disable
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args: [<plugin>].
	 * @param array $assoc_args Associative args.
	 */
	public function force_disable( $args, $assoc_args ) {
		$this->ensure_capabilities();
		if ( empty( $args[0] ) ) {
			WP_CLI::error( __( 'Missing <plugin> argument.', 'blaminhor-essentials' ) );
		}
		$slug = sanitize_text_field( (string) $args[0] );

		WP_CLI::confirm(
			sprintf(
				/* translators: %s: plugin slug */
				__( 'Force-disable plugin "%s" via direct active_plugins edit (skips deactivation hooks)?', 'blaminhor-essentials' ),
				$slug
			),
			$assoc_args
		);

		$plugin_file = $this->find_plugin_file( $slug );
		if ( ! $plugin_file ) {
			WP_CLI::error( sprintf(
				/* translators: %s: slug typed by the user */
				__( 'No plugin found matching "%s".', 'blaminhor-essentials' ),
				$slug
			) );
		}

		$active = (array) get_option( 'active_plugins', array() );
		$idx    = array_search( $plugin_file, $active, true );
		if ( false === $idx ) {
			WP_CLI::warning( sprintf(
				/* translators: %s: plugin file */
				__( 'Plugin "%s" is not in active_plugins.', 'blaminhor-essentials' ),
				$plugin_file
			) );
			return;
		}
		unset( $active[ $idx ] );
		update_option( 'active_plugins', array_values( $active ) );

		blaminhor_essentials_log( sprintf( 'FER: plugin "%s" force-disabled via WP-CLI.', $plugin_file ), 'warning', 'cli', array( 'event' => 'fer_force_disable' ) );
		WP_CLI::success( sprintf(
			/* translators: %s: plugin file */
			__( 'Plugin "%s" removed from active_plugins.', 'blaminhor-essentials' ),
			$plugin_file
		) );
	}

	/**
	 * Force-enable a plugin by editing active_plugins directly.
	 *
	 * ## OPTIONS
	 *
	 * <plugin>
	 * : Plugin slug (folder name) OR full path (folder/file.php).
	 *
	 * @subcommand force-enable
	 * @when after_wp_load
	 *
	 * @param array $args       Positional args: [<plugin>].
	 * @param array $assoc_args Associative args (unused).
	 */
	public function force_enable( $args, $assoc_args ) {
		$this->ensure_capabilities();
		if ( empty( $args[0] ) ) {
			WP_CLI::error( __( 'Missing <plugin> argument.', 'blaminhor-essentials' ) );
		}
		$slug        = sanitize_text_field( (string) $args[0] );
		$plugin_file = $this->find_plugin_file( $slug );
		if ( ! $plugin_file ) {
			WP_CLI::error( sprintf(
				/* translators: %s: slug typed by the user */
				__( 'No plugin found matching "%s".', 'blaminhor-essentials' ),
				$slug
			) );
		}

		$active = (array) get_option( 'active_plugins', array() );
		if ( in_array( $plugin_file, $active, true ) ) {
			WP_CLI::warning( sprintf(
				/* translators: %s: plugin file */
				__( 'Plugin "%s" is already active.', 'blaminhor-essentials' ),
				$plugin_file
			) );
			return;
		}
		$active[] = $plugin_file;
		update_option( 'active_plugins', $active );

		blaminhor_essentials_log( sprintf( 'FER: plugin "%s" force-enabled via WP-CLI.', $plugin_file ), 'warning', 'cli', array( 'event' => 'fer_force_enable' ) );
		WP_CLI::success( sprintf(
			/* translators: %s: plugin file */
			__( 'Plugin "%s" added to active_plugins.', 'blaminhor-essentials' ),
			$plugin_file
		) );
	}

	/**
	 * Resolve a slug or path to an `active_plugins`-style file reference.
	 *
	 * @param string $input Folder name or folder/file.php.
	 * @return string|false
	 */
	private function find_plugin_file( $input ) {
		if ( false !== strpos( $input, '/' ) && '.php' === strtolower( substr( $input, -4 ) ) ) {
			// Already a folder/file.php path.
			$candidate = trailingslashit( WP_PLUGIN_DIR ) . $input;
			if ( file_exists( $candidate ) ) {
				return $input;
			}
			return false;
		}

		$folder = trailingslashit( WP_PLUGIN_DIR ) . $input;
		if ( ! is_dir( $folder ) ) {
			return false;
		}

		// Same-name convention (foo/foo.php).
		$same = $input . '/' . $input . '.php';
		if ( file_exists( trailingslashit( WP_PLUGIN_DIR ) . $same ) ) {
			return $same;
		}

		// Scan for the first PHP file containing a Plugin Name header.
		foreach ( (array) glob( trailingslashit( $folder ) . '*.php' ) as $php ) {
			$head = @file_get_contents( $php, false, null, 0, 8192 );
			if ( $head && false !== strpos( $head, 'Plugin Name:' ) ) {
				return $input . '/' . basename( $php );
			}
		}
		return false;
	}
}
