<?php
/**
 * Self-hosted plugin updates (Reborn Labs download channel).
 */
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

if ( class_exists( 'DC_V2_Plugin_Updater', false ) ) {
	return;
}

class DC_V2_Plugin_Updater {

	const META_URL = 'https://rebornlabs.me/downloads/order-from-dm.json';
	const TRANSIENT_META = 'dc_v2_remote_update_meta';
	const OPTION_AUTO_UPDATE = 'dc_v2_auto_update_enabled';
	const OPTION_LAST_KNOWN_VERSION = 'dc_v2_last_known_version';
	const OPTION_NOTICE_VERSION = 'dc_v2_update_notice_version';
	const OPTION_DISMISSED_NOTICE = 'dc_v2_dismissed_update_notice_version';

	private static $booted = false;

	/** @var string|null In-memory backup of secrets.php during a plugin upgrade. */
	private static $secrets_backup = null;

	public static function init() {
		if ( self::$booted ) {
			return;
		}
		self::$booted = true;

		add_filter( 'pre_set_site_transient_update_plugins', array( __CLASS__, 'inject_update' ) );
		add_filter( 'site_transient_update_plugins', array( __CLASS__, 'inject_update_on_read' ) );
		add_filter( 'plugins_api', array( __CLASS__, 'plugin_info' ), 10, 3 );
		add_filter( 'auto_update_plugin', array( __CLASS__, 'maybe_auto_update' ), 10, 2 );
		// Soft refresh only on Plugins screen — never wipe caches on every OFDM page load
		// (that forced remote fetches across wp-admin and froze the dashboard).
		add_action( 'load-plugins.php', array( __CLASS__, 'maybe_soft_refresh_metadata' ) );
		add_action( 'load-plugins.php', array( __CLASS__, 'maybe_focus_plugin_row' ) );
		add_action( 'rest_api_init', array( __CLASS__, 'register_routes' ) );
		add_action( 'admin_init', array( __CLASS__, 'track_version_change' ), 5 );
		add_filter( 'upgrader_source_selection', array( __CLASS__, 'map_package_to_installed_folder' ), 10, 4 );
		// ZIP never ships secrets.php — backup/restore across WP package overwrite.
		add_filter( 'upgrader_pre_install', array( __CLASS__, 'backup_secrets_before_upgrade' ), 10, 2 );
		add_filter( 'upgrader_post_install', array( __CLASS__, 'restore_secrets_after_upgrade' ), 10, 2 );
		add_action( 'admin_init', array( __CLASS__, 'maybe_persist_durable_secrets' ), 20 );
	}

	/**
	 * Path to durable secrets outside the plugin directory.
	 */
	public static function durable_secrets_path() {
		if ( ! defined( 'WP_CONTENT_DIR' ) ) {
			return '';
		}
		return trailingslashit( WP_CONTENT_DIR ) . 'ofdm-secrets.php';
	}

	/**
	 * Snapshot secrets.php before WordPress deletes the plugin folder.
	 *
	 * @param bool|WP_Error $return
	 * @param array         $extra
	 * @return bool|WP_Error
	 */
	public static function backup_secrets_before_upgrade( $return, $extra ) {
		if ( is_wp_error( $return ) ) {
			return $return;
		}
		if ( empty( $extra['plugin'] ) || $extra['plugin'] !== self::plugin_file() ) {
			return $return;
		}

		self::$secrets_backup = null;
		$candidates           = array();
		if ( defined( 'DC_V2_PATH' ) ) {
			$candidates[] = DC_V2_PATH . 'includes/v2/secrets.php';
		}
		$durable = self::durable_secrets_path();
		if ( $durable ) {
			$candidates[] = $durable;
		}

		foreach ( $candidates as $path ) {
			if ( is_readable( $path ) ) {
				// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
				$contents = file_get_contents( $path );
				if ( is_string( $contents ) && $contents !== '' ) {
					self::$secrets_backup = $contents;
					break;
				}
			}
		}

		return $return;
	}

	/**
	 * Put secrets.php back after the new ZIP is extracted (ZIP omits it on purpose).
	 *
	 * @param bool|WP_Error $return
	 * @param array         $extra
	 * @return bool|WP_Error
	 */
	public static function restore_secrets_after_upgrade( $return, $extra ) {
		if ( empty( $extra['plugin'] ) || $extra['plugin'] !== self::plugin_file() ) {
			return $return;
		}
		if ( ! is_string( self::$secrets_backup ) || self::$secrets_backup === '' ) {
			return $return;
		}

		$plugin_secrets = '';
		if ( defined( 'DC_V2_PATH' ) ) {
			$plugin_secrets = DC_V2_PATH . 'includes/v2/secrets.php';
		} else {
			$plugin_secrets = WP_PLUGIN_DIR . '/' . dirname( self::plugin_file() ) . '/includes/v2/secrets.php';
		}

		self::write_secrets_file( $plugin_secrets, self::$secrets_backup );
		$durable = self::durable_secrets_path();
		if ( $durable ) {
			self::write_secrets_file( $durable, self::$secrets_backup );
		}
		self::$secrets_backup = null;

		return $return;
	}

	/**
	 * Keep a copy under wp-content/ so future updates can recover even if in-memory backup fails.
	 */
	public static function maybe_persist_durable_secrets() {
		if ( ! current_user_can( 'manage_options' ) && ! current_user_can( 'manage_woocommerce' ) ) {
			return;
		}
		if ( ! defined( 'DC_V2_PATH' ) ) {
			return;
		}
		$plugin_secrets = DC_V2_PATH . 'includes/v2/secrets.php';
		$durable        = self::durable_secrets_path();
		if ( ! $durable || ! is_readable( $plugin_secrets ) ) {
			return;
		}
		if ( is_readable( $durable ) ) {
			return;
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
		$contents = file_get_contents( $plugin_secrets );
		if ( is_string( $contents ) && $contents !== '' ) {
			self::write_secrets_file( $durable, $contents );
		}
	}

	/**
	 * @param string $path
	 * @param string $contents
	 */
	private static function write_secrets_file( $path, $contents ) {
		$path = (string) $path;
		if ( $path === '' || $contents === '' ) {
			return;
		}
		$dir = dirname( $path );
		if ( ! is_dir( $dir ) ) {
			wp_mkdir_p( $dir );
		}
		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents
		@file_put_contents( $path, $contents );
		if ( function_exists( 'chmod' ) ) {
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod
			@chmod( $path, 0600 );
		}
	}

	/**
	 * Link to Plugins list filtered to this plugin, with scroll-into-view on load.
	 */
	public static function plugins_page_url() {
		$url = add_query_arg(
			array(
				's'            => 'Order From DM',
				'plugin_status'=> 'all',
				'dc_v2_focus'  => '1',
			),
			admin_url( 'plugins.php' )
		);
		$row_id = sanitize_title( self::plugin_file() );
		return $url . '#' . $row_id;
	}

	/**
	 * On plugins.php?dc_v2_focus=1, scroll/highlight our plugin row.
	 */
	public static function maybe_focus_plugin_row() {
		if ( empty( $_GET['dc_v2_focus'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}
		$plugin = self::plugin_file();
		add_action(
			'admin_footer',
			static function () use ( $plugin ) {
				?>
				<script>
				(function () {
					var plugin = <?php echo wp_json_encode( $plugin ); ?>;
					var rowId = <?php echo wp_json_encode( sanitize_title( $plugin ) ); ?>;
					var row = null;
					var rows = document.querySelectorAll('tr[data-plugin]');
					for (var i = 0; i < rows.length; i++) {
						if (rows[i].getAttribute('data-plugin') === plugin) {
							row = rows[i];
							break;
						}
					}
					if (!row) row = document.getElementById(rowId);
					if (!row) return;
					row.scrollIntoView({ block: 'center', behavior: 'smooth' });
					row.style.outline = '2px solid #2271b1';
					row.style.outlineOffset = '2px';
					window.setTimeout(function () {
						row.style.outline = '';
						row.style.outlineOffset = '';
					}, 4000);
				})();
				</script>
				<?php
			}
		);
	}

	/**
	 * At most once every 30 minutes on plugins.php: refresh our meta without
	 * deleting WordPress' update_plugins transient (avoids admin-wide rebuilds).
	 */
	public static function maybe_soft_refresh_metadata() {
		$lock = get_site_transient( 'dc_v2_update_soft_refresh_lock' );
		if ( $lock ) {
			return;
		}
		set_site_transient( 'dc_v2_update_soft_refresh_lock', 1, 30 * MINUTE_IN_SECONDS );
		delete_site_transient( self::TRANSIENT_META );
		delete_site_transient( 'dc_v2_remote_update_meta_fail' );
		// One cheap fetch; short timeout. Do not delete update_plugins.
		self::get_remote_metadata( true );
	}

	/** @deprecated Use maybe_soft_refresh_metadata() */
	public static function refresh_metadata_on_plugins_screen() {
		self::maybe_soft_refresh_metadata();
	}

	/**
	 * Inject update metadata when WP reads the plugins transient.
	 * Uses cached remote meta only — never force a network fetch on read
	 * (get_site_transient( 'update_plugins' ) runs across wp-admin).
	 */
	public static function inject_update_on_read( $transient ) {
		return self::inject_update( $transient, true );
	}

	public static function plugin_file() {
		return plugin_basename( DC_V2_FILE );
	}

	public static function register_routes() {
		if ( ! class_exists( 'DC_V2_REST' ) ) {
			return;
		}

		$admin = array( 'permission_callback' => array( 'DC_V2_REST', 'can_manage' ) );

		register_rest_route( DC_V2_REST::NS, '/admin/updates', array(
			array(
				'methods'             => 'GET',
				'callback'            => array( __CLASS__, 'rest_get_state' ),
				'permission_callback' => $admin['permission_callback'],
			),
			array(
				'methods'             => 'POST',
				'callback'            => array( __CLASS__, 'rest_save_settings' ),
				'permission_callback' => $admin['permission_callback'],
			),
		) );

		register_rest_route( DC_V2_REST::NS, '/admin/updates/dismiss-notice', array(
			'methods'             => 'POST',
			'callback'            => array( __CLASS__, 'rest_dismiss_notice' ),
			'permission_callback' => $admin['permission_callback'],
		) );

		register_rest_route( DC_V2_REST::NS, '/admin/updates/run', array(
			'methods'             => 'POST',
			'callback'            => array( __CLASS__, 'rest_run_upgrade' ),
			'permission_callback' => array( __CLASS__, 'can_run_upgrade' ),
		) );
	}

	public static function can_run_upgrade() {
		if ( current_user_can( 'update_plugins' ) ) {
			return true;
		}
		// In-app update for anyone who can open Order From DM admin (no update.php redirect).
		return class_exists( 'DC_V2_REST', false ) && DC_V2_REST::can_manage();
	}

	/**
	 * ZIP packages use folder order-from-dm/; dev installs may live in amispet-direct-checkout-v2/.
	 * Rename extracted source so WordPress upgrades the active folder instead of adding a duplicate.
	 */
	public static function map_package_to_installed_folder( $source, $remote_source, $upgrader, $extra ) {
		unset( $remote_source, $upgrader );

		if ( empty( $extra['plugin'] ) || $extra['plugin'] !== self::plugin_file() ) {
			return $source;
		}

		$installed_slug = dirname( self::plugin_file() );
		$package_slug   = basename( wp_normalize_path( untrailingslashit( (string) $source ) ) );

		if ( $package_slug === $installed_slug || ! is_dir( $source ) ) {
			return $source;
		}

		$target = trailingslashit( dirname( $source ) ) . $installed_slug;
		if ( is_dir( $target ) ) {
			global $wp_filesystem;
			if ( empty( $wp_filesystem ) ) {
				require_once ABSPATH . 'wp-admin/includes/file.php';
				WP_Filesystem();
			}
			if ( $wp_filesystem && $wp_filesystem->is_dir( $target ) ) {
				$wp_filesystem->delete( $target, true );
			}
		}

		// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
		if ( @rename( $source, $target ) ) {
			return $target;
		}

		return $source;
	}

	public static function track_version_change() {
		$last = (string) get_option( self::OPTION_LAST_KNOWN_VERSION, '' );
		if ( $last === DC_V2_VERSION ) {
			return;
		}
		if ( $last && version_compare( DC_V2_VERSION, $last, '>' ) ) {
			update_option( self::OPTION_NOTICE_VERSION, DC_V2_VERSION );
		}
		update_option( self::OPTION_LAST_KNOWN_VERSION, DC_V2_VERSION );
	}

	/** Cheap check using cached metadata only — never hits the network. */
	public static function is_update_available_cached() {
		$cached = get_site_transient( self::TRANSIENT_META );
		return is_object( $cached )
			&& ! empty( $cached->version )
			&& version_compare( DC_V2_VERSION, (string) $cached->version, '<' );
	}

	public static function get_remote_metadata( $force = false ) {
		if ( ! $force ) {
			$cached = get_site_transient( self::TRANSIENT_META );
			if ( is_object( $cached ) && ! empty( $cached->version ) ) {
				return $cached;
			}
			// Negative cache: after a failed/slow fetch, do not block every admin request.
			if ( get_site_transient( 'dc_v2_remote_update_meta_fail' ) ) {
				return null;
			}
		}

		$response = wp_remote_get(
			add_query_arg( 'cb', time(), self::META_URL ),
			array(
				'timeout' => 3,
				'headers' => array(
					'Accept' => 'application/json',
				),
			)
		);

		if ( is_wp_error( $response ) ) {
			set_site_transient( 'dc_v2_remote_update_meta_fail', 1, 10 * MINUTE_IN_SECONDS );
			return null;
		}

		$code = (int) wp_remote_retrieve_response_code( $response );
		if ( $code < 200 || $code >= 300 ) {
			set_site_transient( 'dc_v2_remote_update_meta_fail', 1, 10 * MINUTE_IN_SECONDS );
			return null;
		}

		$data = json_decode( wp_remote_retrieve_body( $response ) );
		if ( ! is_object( $data ) || empty( $data->version ) || empty( $data->download_url ) ) {
			set_site_transient( 'dc_v2_remote_update_meta_fail', 1, 10 * MINUTE_IN_SECONDS );
			return null;
		}

		if ( empty( $data->changelog ) || ! is_array( $data->changelog ) ) {
			$data->changelog = array();
		}

		delete_site_transient( 'dc_v2_remote_update_meta_fail' );
		set_site_transient( self::TRANSIENT_META, $data, 6 * HOUR_IN_SECONDS );
		return $data;
	}

	public static function inject_update( $transient, $on_read = false ) {
		if ( ! is_object( $transient ) ) {
			$transient = new stdClass();
		}

		if ( empty( $transient->checked ) || ! is_array( $transient->checked ) ) {
			if ( ! $on_read ) {
				return $transient;
			}
			$transient->checked = array();
		}

		$plugin = self::plugin_file();
		if ( ! isset( $transient->checked[ $plugin ] ) ) {
			$transient->checked[ $plugin ] = DC_V2_VERSION;
		}

		// Never force remote here. On-read used to pass $on_read as $force and
		// hammered META_URL on every wp-admin page (15s timeout). Cache miss still
		// fetches once via get_remote_metadata( false ).
		$remote = self::get_remote_metadata( false );
		if ( ! $remote ) {
			return $transient;
		}

		$item = self::build_update_item( $remote, $plugin );

		if ( version_compare( DC_V2_VERSION, $remote->version, '<' ) ) {
			// Keep our admin updater; hide WP core Plugins-screen link for roles without update_plugins.
			if ( current_user_can( 'update_plugins' ) ) {
				if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
					$transient->response = array();
				}
				$transient->response[ $plugin ] = $item;
				if ( isset( $transient->no_update[ $plugin ] ) ) {
					unset( $transient->no_update[ $plugin ] );
				}
			}
		} else {
			if ( ! isset( $transient->no_update ) || ! is_array( $transient->no_update ) ) {
				$transient->no_update = array();
			}
			$transient->no_update[ $plugin ] = $item;
		}

		return $transient;
	}

	private static function build_update_item( $remote, $plugin ) {
		return (object) array(
			'id'           => $plugin,
			'slug'         => dirname( $plugin ),
			'plugin'       => $plugin,
			'new_version'  => $remote->version,
			'url'          => ! empty( $remote->homepage ) ? $remote->homepage : 'https://orderfromdm.rebornlabs.me/',
			'package'      => $remote->download_url,
			'tested'       => ! empty( $remote->tested ) ? $remote->tested : '',
			'requires'     => ! empty( $remote->requires ) ? $remote->requires : '5.8',
			'requires_php' => ! empty( $remote->requires_php ) ? $remote->requires_php : '7.4',
		);
	}

	public static function plugin_info( $result, $action, $args ) {
		if ( 'plugin_information' !== $action || empty( $args->slug ) ) {
			return $result;
		}

		$plugin = self::plugin_file();
		if ( dirname( $plugin ) !== $args->slug ) {
			return $result;
		}

		$remote = self::get_remote_metadata();
		if ( ! $remote ) {
			return $result;
		}

		$changelog_html = self::changelog_html( $remote->changelog );
		$sections       = array(
			'description' => __( 'Shared order links with a DM-style conversational checkout for assisted sales and bank transfer payments.', 'order-from-dm' ),
			'changelog'   => $changelog_html,
		);

		return (object) array(
			'name'          => dc_v2_plugin_title(),
			'slug'          => dirname( $plugin ),
			'version'       => $remote->version,
			'author'        => '<a href="https://rebornlabs.me">Reborn Labs</a>',
			'homepage'      => ! empty( $remote->homepage ) ? $remote->homepage : 'https://orderfromdm.rebornlabs.me/',
			'download_link' => $remote->download_url,
			'sections'      => $sections,
			'tested'        => ! empty( $remote->tested ) ? $remote->tested : '',
			'requires'      => ! empty( $remote->requires ) ? $remote->requires : '5.8',
			'requires_php'  => ! empty( $remote->requires_php ) ? $remote->requires_php : '7.4',
		);
	}

	public static function maybe_auto_update( $update, $item ) {
		if ( ! self::is_auto_update_enabled() ) {
			return $update;
		}
		$plugin = self::plugin_file();
		if ( is_object( $item ) && isset( $item->plugin ) && $item->plugin === $plugin ) {
			return true;
		}
		return $update;
	}

	public static function is_auto_update_enabled() {
		$plugin = self::plugin_file();
		$auto   = (array) get_site_option( 'auto_update_plugins', array() );
		return in_array( $plugin, $auto, true );
	}

	public static function set_auto_update( $enabled ) {
		$plugin = self::plugin_file();
		$auto   = (array) get_site_option( 'auto_update_plugins', array() );
		if ( $enabled ) {
			if ( ! in_array( $plugin, $auto, true ) ) {
				$auto[] = $plugin;
			}
			update_option( self::OPTION_AUTO_UPDATE, 1 );
		} else {
			$auto = array_values( array_diff( $auto, array( $plugin ) ) );
			update_option( self::OPTION_AUTO_UPDATE, 0 );
		}
		update_site_option( 'auto_update_plugins', array_values( array_unique( $auto ) ) );
	}

	public static function update_url() {
		if ( ! current_user_can( 'update_plugins' ) ) {
			return '';
		}
		$plugin = self::plugin_file();
		return wp_nonce_url(
			admin_url( 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $plugin ) ),
			'upgrade-plugin_' . $plugin
		);
	}

	public static function admin_state( $refresh = false ) {
		$remote           = self::get_remote_metadata( $refresh );
		$latest           = $remote ? (string) $remote->version : DC_V2_VERSION;
		$update_available = $remote && version_compare( DC_V2_VERSION, $latest, '<' );
		$notice_version   = (string) get_option( self::OPTION_NOTICE_VERSION, '' );
		$dismissed        = (string) get_option( self::OPTION_DISMISSED_NOTICE, '' );
		$show_notice      = $notice_version !== ''
			&& version_compare( $notice_version, DC_V2_VERSION, '<=' )
			&& $dismissed !== DC_V2_VERSION;

		$changelog = $remote ? array_values( array_map( 'strval', $remote->changelog ) ) : array();

		return array(
			'installedVersion'   => DC_V2_VERSION,
			'latestVersion'      => $latest,
			'updateAvailable'    => $update_available,
			'updateUrl'          => $update_available ? self::update_url() : '',
			'pluginsPageUrl'     => self::plugins_page_url(),
			'autoUpdate'         => self::is_auto_update_enabled(),
			'changelog'          => $changelog,
			'installedChangelog' => self::bundled_changelog(),
			'updateNotice'      => array(
				'show'      => $show_notice,
				'version'   => DC_V2_VERSION,
				'changelog' => $changelog,
			),
		);
	}

	public static function rest_get_state( WP_REST_Request $request ) {
		$refresh = (bool) $request->get_param( 'refresh' );
		return rest_ensure_response( self::admin_state( $refresh ) );
	}

	public static function rest_save_settings( WP_REST_Request $request ) {
		$body = $request->get_json_params();
		if ( ! is_array( $body ) ) {
			return new WP_Error( 'dc_v2_invalid_body', __( 'Invalid request body.', 'order-from-dm' ), array( 'status' => 400 ) );
		}
		if ( array_key_exists( 'autoUpdate', $body ) ) {
			self::set_auto_update( (bool) $body['autoUpdate'] );
		}
		delete_site_transient( self::TRANSIENT_META );
		return rest_ensure_response( self::admin_state( true ) );
	}

	public static function rest_dismiss_notice() {
		update_option( self::OPTION_DISMISSED_NOTICE, DC_V2_VERSION );
		return rest_ensure_response( self::admin_state() );
	}

	public static function rest_run_upgrade() {
		if ( ! self::can_run_upgrade() ) {
			return new WP_Error(
				'dc_v2_forbidden',
				__( 'You do not have permission to update plugins.', 'order-from-dm' ),
				array( 'status' => 403 )
			);
		}

		delete_site_transient( self::TRANSIENT_META );
		delete_site_transient( 'update_plugins' );

		// Ensure WordPress sees the latest package metadata before upgrading.
		wp_update_plugins();

		$remote = self::get_remote_metadata( true );
		if ( ! $remote || ! version_compare( DC_V2_VERSION, $remote->version, '<' ) ) {
			return new WP_Error(
				'dc_v2_no_update',
				__( 'No update is available right now.', 'order-from-dm' ),
				array( 'status' => 400 )
			);
		}

		$fs_error = self::ensure_upgrade_filesystem();
		if ( is_wp_error( $fs_error ) ) {
			return $fs_error;
		}

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

		$plugin     = self::plugin_file();
		$was_active = is_plugin_active( $plugin );
		$skin       = new Automatic_Upgrader_Skin();
		$upgrader   = new Plugin_Upgrader( $skin );
		$result     = $upgrader->upgrade( $plugin );

		if ( is_wp_error( $result ) ) {
			self::restore_plugin_active_state( $plugin, $was_active );
			return $result;
		}

		if ( false === $result ) {
			$message = __( 'The plugin could not be updated. Try again from Order From DM settings.', 'order-from-dm' );
			if ( is_wp_error( $skin->result ) ) {
				$message = $skin->result->get_error_message();
			}
			self::restore_plugin_active_state( $plugin, $was_active );
			return new WP_Error( 'dc_v2_upgrade_failed', $message, array( 'status' => 500 ) );
		}

		$reactivated = self::restore_plugin_active_state( $plugin, $was_active );
		if ( is_wp_error( $reactivated ) ) {
			return new WP_Error(
				'dc_v2_reactivate_failed',
				sprintf(
					/* translators: 1: plugin version number, 2: error message */
					__( 'Order From DM was updated to %1$s but could not be reactivated: %2$s', 'order-from-dm' ),
					$remote->version,
					$reactivated->get_error_message()
				),
				array( 'status' => 500 )
			);
		}

		$verify = self::verify_installed_version( $plugin, (string) $remote->version );
		if ( is_wp_error( $verify ) ) {
			return $verify;
		}

		delete_site_transient( self::TRANSIENT_META );
		delete_site_transient( 'update_plugins' );

		return rest_ensure_response(
			array(
				'upgraded' => true,
				'version'  => (string) $remote->version,
				'message'  => sprintf(
					/* translators: %s: plugin version number */
					__( 'Order From DM was updated to %s.', 'order-from-dm' ),
					$remote->version
				),
				'updates'  => self::admin_state( true ),
			)
		);
	}

	/**
	 * WordPress deactivates plugins before upgrading but only reactivates on update.php / cron.
	 */
	private static function restore_plugin_active_state( $plugin, $was_active ) {
		if ( ! $was_active || is_plugin_active( $plugin ) ) {
			return true;
		}

		$activated = activate_plugin( $plugin, '', false, true );
		if ( is_wp_error( $activated ) ) {
			return $activated;
		}

		return true;
	}

	private static function ensure_upgrade_filesystem() {
		require_once ABSPATH . 'wp-admin/includes/file.php';

		if ( ! WP_Filesystem() ) {
			return new WP_Error(
				'dc_v2_filesystem',
				__( 'WordPress could not access the filesystem to install the update.', 'order-from-dm' ),
				array( 'status' => 500 )
			);
		}

		return true;
	}

	private static function verify_installed_version( $plugin, $expected ) {
		$path = WP_PLUGIN_DIR . '/' . ltrim( $plugin, '/' );
		if ( ! is_readable( $path ) ) {
			return new WP_Error(
				'dc_v2_verify_missing',
				__( 'The update finished but the plugin file could not be verified.', 'order-from-dm' ),
				array( 'status' => 500 )
			);
		}

		$headers   = get_file_data( $path, array( 'Version' => 'Version' ) );
		$installed = isset( $headers['Version'] ) ? (string) $headers['Version'] : '';

		if ( $installed === '' || version_compare( $installed, $expected, '<' ) ) {
			return new WP_Error(
				'dc_v2_version_mismatch',
				sprintf(
					/* translators: 1: installed version, 2: expected version */
					__( 'The downloaded package was outdated or incomplete (got %1$s, expected %2$s). Wait a minute and try again.', 'order-from-dm' ),
					$installed !== '' ? $installed : '?',
					$expected
				),
				array( 'status' => 500 )
			);
		}

		return true;
	}

	private static function bundled_changelog() {
		$path = DC_V2_PATH . 'includes/v2/release-changelog.json';
		if ( ! is_readable( $path ) ) {
			return array();
		}
		$data = json_decode( (string) file_get_contents( $path ), true );
		if ( ! is_array( $data ) ) {
			return array();
		}
		return array_values( array_map( 'strval', $data ) );
	}

	private static function changelog_html( $items ) {
		if ( empty( $items ) || ! is_array( $items ) ) {
			return '<p>' . esc_html__( 'Thank you for keeping Order From DM up to date.', 'order-from-dm' ) . '</p>';
		}
		$lis = array();
		foreach ( $items as $item ) {
			$lis[] = '<li>' . esc_html( (string) $item ) . '</li>';
		}
		return '<ul>' . implode( '', $lis ) . '</ul>';
	}
}
