<?php
/**
 * Class VSERPB_Update_Handler
 *
 * Hooks into the WordPress.org plugin update lifecycle to:
 *
 *  1. Show a pre-update notice in the Plugins list row (in_plugin_update_message).
 *  2. Detect success or failure after the update runs (upgrader_process_complete).
 *  3. Display a precise, actionable admin notice for every known failure reason.
 *
 * Supports both single-plugin and bulk-update flows.
 */

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

class VSERPB_Update_Handler {

	/**
	 * Plugin basename — e.g. voidek-serp-schema-builder/voidek-serp-schema-builder.php
	 */
	const BASENAME = 'voidek-serp-schema-builder/voidek-serp-schema-builder.php';

	/**
	 * Option key used to persist a failure notice across the redirect that
	 * WordPress does after an update attempt.
	 */
	const NOTICE_OPTION = 'vserpb_update_notice';

	/**
	 * Register all hooks.
	 */
	public static function init() {
		// 1. Pre-update warning row appended under the plugin entry.
		add_action(
			'in_plugin_update_message-' . self::BASENAME,
			array( __CLASS__, 'show_pre_update_message' ),
			10,
			2
		);

		// 2. Post-update hook — fires after every upgrader run.
		add_action( 'upgrader_process_complete', array( __CLASS__, 'handle_update_complete' ), 10, 2 );

		// 3. Display any stored notice on the next admin page load.
		add_action( 'admin_notices', array( __CLASS__, 'display_stored_notice' ) );
	}

	// =========================================================================
	// 1. PRE-UPDATE MESSAGE
	// =========================================================================

	/**
	 * Minimum free disk space (in bytes) recommended for a safe update.
	 * 50 MB — enough for the zip download, extraction, and old-version removal.
	 */
	const MIN_FREE_DISK_BYTES = 52428800; // 50 MB

	/**
	 * Minimum PHP memory limit (in bytes) recommended for running the upgrader.
	 */
	const MIN_MEMORY_BYTES = 67108864; // 64 MB

	/**
	 * Minimum PHP max_execution_time (seconds) recommended for an update.
	 */
	const MIN_EXEC_SECONDS = 30;

	// =========================================================================
	// SERVER RESOURCE HELPERS
	// =========================================================================

	/**
	 * Convert a PHP ini shorthand value (e.g. "128M", "1G", "-1") to bytes.
	 *
	 * @param  string|int $value
	 * @return int  Bytes, or PHP_INT_MAX when the value is -1 / 0 (unlimited).
	 */
	private static function parse_ini_bytes( $value ) {
		$value = trim( (string) $value );
		if ( '-1' === $value || '0' === $value ) {
			return PHP_INT_MAX;
		}
		$last  = strtolower( substr( $value, -1 ) );
		$bytes = (int) $value;
		switch ( $last ) {
			case 'g': $bytes *= 1024;
			// fall through
			case 'm': $bytes *= 1024;
			// fall through
			case 'k': $bytes *= 1024;
		}
		return $bytes;
	}

	/**
	 * Return free disk space for the plugins directory, or false if unavailable.
	 *
	 * @return int|false
	 */
	private static function get_free_disk_bytes() {
		if ( ! function_exists( 'disk_free_space' ) ) {
			return false;
		}
		$dir  = defined( 'WP_PLUGIN_DIR' ) ? WP_PLUGIN_DIR : ABSPATH . 'wp-content/plugins';
		if ( is_readable( $dir ) ) {
    $free = disk_free_space( $dir );
} // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		return ( false !== $free ) ? (int) $free : false;
	}

	/**
	 * Return the PHP memory limit in bytes (PHP_INT_MAX when unlimited).
	 *
	 * @return int
	 */
	private static function get_memory_limit_bytes() {
		return self::parse_ini_bytes( ini_get( 'memory_limit' ) );
	}

	/**
	 * Return the PHP max_execution_time in seconds (PHP_INT_MAX when unlimited).
	 *
	 * @return int
	 */
	private static function get_max_exec_seconds() {
		$val = (int) ini_get( 'max_execution_time' );
		return ( 0 === $val ) ? PHP_INT_MAX : $val;
	}

	/**
	 * Human-readable byte size (KB / MB / GB).
	 *
	 * @param  int    $bytes
	 * @return string
	 */
	private static function format_bytes( $bytes ) {
		if ( $bytes >= 1073741824 ) {
			return round( $bytes / 1073741824, 1 ) . ' GB';
		}
		if ( $bytes >= 1048576 ) {
			return round( $bytes / 1048576, 1 ) . ' MB';
		}
		return round( $bytes / 1024, 1 ) . ' KB';
	}

	// =========================================================================
	// 1. PRE-UPDATE MESSAGE
	// =========================================================================

	/**
	 * Appended inside the plugin row update notice (the yellow bar in Plugins list).
	 * Checks server resources and compatibility before the admin clicks Update.
	 *
	 * @param array    $plugin_data  Plugin header data from the repository.
	 * @param stdClass $response     Update transient response object.
	 */
	public static function show_pre_update_message( $plugin_data, $response ) {
		if ( empty( $response->new_version ) ) {
			return;
		}

		$new      = $response->new_version;
		$warnings = array(); // Blockers — strongly advise resolving first.
		$notices  = array(); // Informational only.

		// ---- PHP version ----
		if ( version_compare( PHP_VERSION, '7.4', '<' ) ) {
			$warnings[] = sprintf(
				/* translators: 1: required PHP, 2: current PHP */
				esc_html__( 'Your server runs PHP %2$s but this plugin requires PHP %1$s or higher. Update PHP before updating the plugin.', 'voidek-serp-schema-builder' ),
				'7.4',
				PHP_VERSION
			);
		}

		// ---- WordPress version ----
		global $wp_version;
		if ( isset( $wp_version ) && version_compare( $wp_version, '5.0', '<' ) ) {
			$warnings[] = sprintf(
				/* translators: 1: required WP version, 2: current WP version */
				esc_html__( 'Your WordPress version is %2$s. This plugin requires WordPress %1$s or higher.', 'voidek-serp-schema-builder' ),
				'5.0',
				$wp_version
			);
		}

		// ---- Disk space ----
		$free_disk = self::get_free_disk_bytes();
		if ( false !== $free_disk && $free_disk < self::MIN_FREE_DISK_BYTES ) {
			$warnings[] = sprintf(
				/* translators: 1: free space available, 2: recommended minimum */
				esc_html__( 'Low disk space: only %1$s free. At least %2$s is recommended to safely download and extract the update. Free up disk space before updating.', 'voidek-serp-schema-builder' ),
				self::format_bytes( $free_disk ),
				self::format_bytes( self::MIN_FREE_DISK_BYTES )
			);
		}

		// ---- PHP memory limit ----
		$memory_limit = self::get_memory_limit_bytes();
		if ( PHP_INT_MAX !== $memory_limit && $memory_limit < self::MIN_MEMORY_BYTES ) {
			$warnings[] = sprintf(
				/* translators: 1: current memory limit, 2: recommended minimum */
				esc_html__( 'Low PHP memory: memory_limit is %1$s. At least %2$s is recommended. Ask your host to increase it, or add define(\'WP_MEMORY_LIMIT\', \'256M\') to wp-config.php.', 'voidek-serp-schema-builder' ),
				self::format_bytes( $memory_limit ),
				self::format_bytes( self::MIN_MEMORY_BYTES )
			);
		}

		// ---- PHP execution time ----
		$max_exec = self::get_max_exec_seconds();
		if ( PHP_INT_MAX !== $max_exec && $max_exec < self::MIN_EXEC_SECONDS ) {
			$warnings[] = sprintf(
				/* translators: 1: current execution time limit in seconds, 2: recommended minimum */
				esc_html__( 'Short PHP execution time: max_execution_time is %1$ds. At least %2$ds is recommended. If the update times out, ask your host to raise this limit or use WP-CLI to update instead.', 'voidek-serp-schema-builder' ),
				$max_exec,
				self::MIN_EXEC_SECONDS
			);
		}

		// ---- WooCommerce ----
		$options = get_option( 'vserpb_product_schema', array() );
		if ( ! empty( $options['vserpb_enable_schema'] ) && ! class_exists( 'WooCommerce' ) ) {
			$notices[] = esc_html__( 'WooCommerce is not active. Product Schema will remain disabled until WooCommerce is installed and activated.', 'voidek-serp-schema-builder' );
		}

		// ---- Render ----
		$has_blockers = ! empty( $warnings );
		$border_color = $has_blockers ? '#dc3232' : '#f0b429';
		$bg_color     = $has_blockers ? '#fef7f7' : '#fff8e1';

		echo '<br><span style="display:block;margin-top:6px;padding:8px 12px;'
			. 'background:' . esc_attr( $bg_color ) . ';border-left:3px solid ' . esc_attr( $border_color ) . ';font-size:12px;">';

		if ( $has_blockers ) {
			echo '<strong>🚫 ' . esc_html__( 'Update not recommended — resolve these issues first:', 'voidek-serp-schema-builder' ) . '</strong>'
				. '<ul style="margin:6px 0 0 16px;list-style:disc;">';
			foreach ( $warnings as $w ) {
				echo '<li style="margin-bottom:3px;">' . esc_html( $w ) . '</li>';
			}
			echo '</ul>';
		} else {
			echo '✅ ' . sprintf(
				/* translators: %s new version number */
				esc_html__( 'Ready to update to version %s. Back up your site first.', 'voidek-serp-schema-builder' ),
				esc_html( $new )
			);
		}

		if ( ! empty( $notices ) ) {
			echo '<ul style="margin:6px 0 0 16px;list-style:disc;color:#555;">';
			foreach ( $notices as $n ) {
				echo '<li>ℹ️ ' . esc_html( $n ) . '</li>';
			}
			echo '</ul>';
		}

		echo '</span>';
	}

	// =========================================================================
	// 2. POST-UPDATE HANDLER
	// =========================================================================

	/**
	 * Fires after every upgrader process (single or bulk).
	 * Determines whether our plugin was in the batch, then checks for errors.
	 *
	 * @param WP_Upgrader $upgrader  Core upgrader instance.
	 * @param array       $hook_extra {
	 *     Extra data about what was updated.
	 *     @type string       $action  'update'|'install'
	 *     @type string       $type    'plugin'|'theme'|'core'
	 *     @type array|string $plugins Plugin basenames (bulk) or single basename.
	 * }
	 */
	public static function handle_update_complete( $upgrader, $hook_extra ) {
		// Only care about plugin updates.
		if (
			! isset( $hook_extra['action'], $hook_extra['type'] ) ||
			'update' !== $hook_extra['action'] ||
			'plugin' !== $hook_extra['type']
		) {
			return;
		}

		// Determine which plugins were in this batch.
		$updated_plugins = array();
		if ( isset( $hook_extra['plugins'] ) && is_array( $hook_extra['plugins'] ) ) {
			// Bulk update.
			$updated_plugins = $hook_extra['plugins'];
		} elseif ( isset( $hook_extra['plugin'] ) ) {
			// Single-plugin update.
			$updated_plugins = array( $hook_extra['plugin'] );
		} elseif (
			isset( $upgrader->skin->plugin ) &&
			! empty( $upgrader->skin->plugin )
		) {
			$updated_plugins = array( $upgrader->skin->plugin );
		}

		if ( ! in_array( self::BASENAME, $updated_plugins, true ) ) {
			return;
		}

		// ---- Detect the outcome ----

		// The upgrader result is the canonical source of truth.
		$result = isset( $upgrader->result ) ? $upgrader->result : null;

		if ( is_wp_error( $result ) ) {
			self::store_notice( 'error', self::decode_wp_error( $result ) );
			return;
		}

		// Skin errors (e.g. filesystem issues reported line-by-line).
		if (
			isset( $upgrader->skin ) &&
			method_exists( $upgrader->skin, 'get_errors' )
		) {
			$skin_errors = $upgrader->skin->get_errors();
			if ( is_wp_error( $skin_errors ) && $skin_errors->get_error_code() ) {
				self::store_notice( 'error', self::decode_wp_error( $skin_errors ) );
				return;
			}
		}

		// result === false means the upgrader itself returned false (e.g. download fail).
		// Before showing a generic message, sniff the skin's feedback buffer for
		// memory exhaustion or timeout clues that WordPress doesn't surface as WP_Error.
		if ( false === $result ) {
			$raw_feedback = '';
			if ( isset( $upgrader->skin ) && method_exists( $upgrader->skin, 'get_upgrade_messages' ) ) {
				$msgs = $upgrader->skin->get_upgrade_messages();
				if ( is_array( $msgs ) ) {
					$raw_feedback = strtolower( implode( ' ', $msgs ) );
				}
			}

			if ( strpos( $raw_feedback, 'memory' ) !== false || strpos( $raw_feedback, 'allowed memory size' ) !== false ) {
				self::store_notice(
					'error',
					sprintf(
						/* translators: %s current memory limit */
						esc_html__( 'Update failed: PHP ran out of memory during the update (current limit: %s). Increase memory_limit in php.ini, or add define(\'WP_MEMORY_LIMIT\', \'256M\'); to wp-config.php, then try again.', 'voidek-serp-schema-builder' ),
						self::format_bytes( self::get_memory_limit_bytes() )
					)
				);
				return;
			}

			if ( strpos( $raw_feedback, 'timeout' ) !== false || strpos( $raw_feedback, 'timed out' ) !== false || strpos( $raw_feedback, 'maximum execution time' ) !== false ) {
				self::store_notice(
					'error',
					sprintf(
						/* translators: %d current max_execution_time in seconds */
						esc_html__( 'Update failed: The server timed out while updating (max_execution_time: %ds). Try again, ask your host to increase this limit, or run: wp plugin update voidek-serp-schema-builder', 'voidek-serp-schema-builder' ),
						self::get_max_exec_seconds() === PHP_INT_MAX ? 0 : self::get_max_exec_seconds()
					)
				);
				return;
			}

			if ( strpos( $raw_feedback, 'disk' ) !== false || strpos( $raw_feedback, 'no space' ) !== false || strpos( $raw_feedback, 'quota' ) !== false ) {
				self::store_notice(
					'error',
					sprintf(
						/* translators: %s free disk space */
						esc_html__( 'Update failed: Your server ran out of disk space during the update. Free up space and try again. Current free space: %s.', 'voidek-serp-schema-builder' ),
						false !== self::get_free_disk_bytes() ? self::format_bytes( self::get_free_disk_bytes() ) : esc_html__( 'unknown', 'voidek-serp-schema-builder' )
					)
				);
				return;
			}

			self::store_notice(
				'error',
				esc_html__( 'The update package could not be downloaded from WordPress.org. Please check your server\'s internet connection and try again.', 'voidek-serp-schema-builder' )
			);
			return;
		}

		// All clear — success.
		$plugin_data = get_plugin_data( WP_PLUGIN_DIR . '/' . self::BASENAME );
		$new_version = isset( $plugin_data['Version'] ) ? $plugin_data['Version'] : '';

		$message = $new_version
			? sprintf(
				/* translators: %s new version number */
				esc_html__( 'Voidek Auto Schema Generator updated successfully to version %s.', 'voidek-serp-schema-builder' ),
				$new_version
			)
			: esc_html__( 'Voidek Auto Schema Generator updated successfully.', 'voidek-serp-schema-builder' );

		// Append a settings link for quick access after the update.
		$settings_url = admin_url( 'admin.php?page=voidek-serp-schema-builder' );
		$message     .= ' <a href="' . esc_url( $settings_url ) . '">'
			. esc_html__( 'Review settings →', 'voidek-serp-schema-builder' )
			. '</a>';

		self::store_notice( 'success', $message, /* raw_html */ true );
	}

	// =========================================================================
	// 3. DISPLAY STORED NOTICE
	// =========================================================================

	/**
	 * On the next admin page load, show the stored update notice and then delete it.
	 * Only shown to users who can manage_options.
	 */
	public static function display_stored_notice() {
	  if ( ! current_user_can( 'manage_options' ) ) {
        return;
    }

    // Only show on the plugins list page — avoids site-wide notice concern.
    $screen = get_current_screen();
    if ( ! $screen || 'plugins' !== $screen->id ) {
        return;
    }

		$notice = get_option( self::NOTICE_OPTION );
		if ( empty( $notice ) || ! is_array( $notice ) ) {
			return;
		}

		delete_option( self::NOTICE_OPTION );

		$type    = isset( $notice['type'] ) ? $notice['type'] : 'info';
		$message = isset( $notice['message'] ) ? $notice['message'] : '';
		$raw     = ! empty( $notice['raw_html'] );

		if ( ! $message ) {
			return;
		}

		echo '<div class="notice notice-' . esc_attr( $type ) . ' is-dismissible vserpb-notice" style="padding:10px 14px;">';
		echo '<p>';

		if ( $raw ) {
			// Message is already escaped by the methods that build it.
			// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			echo $message;
		} else {
			echo esc_html( $message );
		}

		echo '</p></div>';
	}

	// =========================================================================
	// INTERNAL HELPERS
	// =========================================================================

	/**
	 * Persist a notice to be displayed on the next page load.
	 *
	 * @param string $type     'success' | 'error' | 'warning' | 'info'
	 * @param string $message  Already-escaped (or raw-HTML) message.
	 * @param bool   $raw_html True if the message contains trusted HTML.
	 */
	private static function store_notice( $type, $message, $raw_html = false ) {
		update_option(
			self::NOTICE_OPTION,
			array(
				'type'     => $type,
				'message'  => $message,
				'raw_html' => $raw_html,
			),
			false // Do not autoload — only needed once after an update.
		);
	}

	/**
	 * Translate a WP_Error from the upgrader into a user-friendly string.
	 * Maps all common WP upgrader error codes to plain-English guidance.
	 *
	 * @param  WP_Error $error
	 * @return string   Escaped, human-readable message.
	 */
	private static function decode_wp_error( WP_Error $error ) {
		$code    = $error->get_error_code();
		$raw_msg = $error->get_error_message();

		// Map known error codes to actionable guidance.
		$known = array(

			// Filesystem / credential errors.
			'unable_to_connect_to_filesystem' => esc_html__(
				'Update failed: WordPress could not connect to the filesystem. Go to Settings → General and ensure "Filesystem Method" credentials are correct, or add define(\'FS_METHOD\', \'direct\') to wp-config.php if your server allows it.',
				'voidek-serp-schema-builder'
			),
			'mkdir_failed'                    => esc_html__(
				'Update failed: Could not create a directory on your server. Check that the /wp-content/plugins/ directory is writable by the web server.',
				'voidek-serp-schema-builder'
			),
			'copy_failed'                     => esc_html__(
				'Update failed: WordPress could not copy plugin files. Check folder permissions in /wp-content/plugins/.',
				'voidek-serp-schema-builder'
			),
			'chmod_failed'                    => esc_html__(
				'Update failed: WordPress could not set file permissions after copying. Check that your web server user owns the plugin folder.',
				'voidek-serp-schema-builder'
			),
			'move_new_plugin_failed'          => esc_html__(
				'Update failed: WordPress could not move the new plugin files into place. The old version is still active. Check folder permissions.',
				'voidek-serp-schema-builder'
			),

			// Download / network errors.
			'download_failed'                 => esc_html__(
				'Update failed: The plugin package could not be downloaded from WordPress.org. Check your server\'s outbound internet connection or firewall rules and try again.',
				'voidek-serp-schema-builder'
			),
			'http_request_failed'             => esc_html__(
				'Update failed: The HTTP request to WordPress.org failed. This may be a temporary network issue — please try again in a few minutes.',
				'voidek-serp-schema-builder'
			),

			// Package / archive errors.
			'incompatible_archive'            => esc_html__(
				'Update failed: The downloaded package was not a valid zip archive. This may be a corrupted download — please try again.',
				'voidek-serp-schema-builder'
			),
			'bad_request'                     => esc_html__(
				'Update failed: WordPress.org returned an unexpected response (bad request). Please try again later or update manually.',
				'voidek-serp-schema-builder'
			),
			'no_package'                      => esc_html__(
				'Update failed: No update package URL was found. The plugin may not be available on WordPress.org yet — please check back later.',
				'voidek-serp-schema-builder'
			),
			'remove_old_failed'               => esc_html__(
				'Update failed: WordPress could not remove the old plugin version before installing the new one. Please deactivate the plugin, delete it manually via FTP, and reinstall.',
				'voidek-serp-schema-builder'
			),
			'could_not_unzip'                 => esc_html__(
				'Update failed: The update package could not be extracted. This usually means your server ran out of disk space or the downloaded file was corrupted. Check disk space and try again.',
				'voidek-serp-schema-builder'
			),

			// Disk / storage errors.
			'disk_full'                       => esc_html__(
				'Update failed: Your server is out of disk space. Delete unused files, plugins, or themes to free up space, then try again.',
				'voidek-serp-schema-builder'
			),
			'disk_full_destination'           => esc_html__(
				'Update failed: No space left on the server to write plugin files. Free up disk space in your hosting control panel and try again.',
				'voidek-serp-schema-builder'
			),

			// Memory errors.
			'memory_exhausted'                => sprintf(
				/* translators: %s current memory limit */
				esc_html__( 'Update failed: PHP ran out of memory (current limit: %s). Increase memory_limit in php.ini or wp-config.php — add define(\'WP_MEMORY_LIMIT\', \'256M\'); — or contact your host.', 'voidek-serp-schema-builder' ),
				self::format_bytes( self::get_memory_limit_bytes() )
			),

			// Timeout errors.
			'timeout'                         => sprintf(
				/* translators: %d current max_execution_time in seconds */
				esc_html__( 'Update failed: The server timed out during the update (max_execution_time: %ds). Try again, or ask your host to increase max_execution_time. You can also update via WP-CLI: wp plugin update voidek-serp-schema-builder', 'voidek-serp-schema-builder' ),
				self::get_max_exec_seconds() === PHP_INT_MAX ? 0 : self::get_max_exec_seconds()
			),
			'request_timeout'                 => esc_html__(
				'Update failed: The download request timed out before WordPress.org could respond. This may be a temporary network slowdown — please try again in a few minutes.',
				'voidek-serp-schema-builder'
			),
		);

		if ( isset( $known[ $code ] ) ) {
			return $known[ $code ];
		}

		// Unknown error — show a generic message with the raw WP message appended
		// so the admin has something to search for or send to support.
		return sprintf(
			/* translators: 1: error code, 2: raw WP error message */
			esc_html__( 'Update failed (%1$s): %2$s — Please try again or update the plugin manually via FTP.', 'voidek-serp-schema-builder' ),
			esc_html( $code ),
			esc_html( wp_strip_all_tags( $raw_msg ) )
		);
	}
}

VSERPB_Update_Handler::init();