<?php

namespace DisableAllUpdates;

use stdClass;
use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID = 'disable-all-updates';
	private const MODULE_VERSION = '1.3.0';

	/** @var array<string, bool> */
	private const DEFAULT_SETTINGS = [
		'disable_core_updates'        => false,
		'disable_plugin_updates'    => false,
		'disable_theme_updates'     => false,
		'disable_translation_updates' => false,
	];

	/** @var array<string, string> Maps update capability to settings key. */
	private const CAP_TO_SETTING = [
		'update_core'      => 'disable_core_updates',
		'update_plugins'   => 'disable_plugin_updates',
		'update_themes'    => 'disable_theme_updates',
		'update_languages' => 'disable_translation_updates',
	];


	private ?array $settings = null;

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Disable All Updates', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Pause automatic updates for core, plugins, themes, or translations when you stage releases yourself. Long-term blocking can leave security gaps—use with a plan to patch manually.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'cron', 'ajax', 'rest' ];
	}

	public function getDependencies(): array {
		return [];
	}

	protected function registerHooks(): void {
		add_filter( 'wp_powersuite_sanitize_module_settings', [ self::class, 'filterSanitizeModuleSettings' ], 10, 3 );
		add_action( 'wp_powersuite_module_settings_saved', [ self::class, 'handleSettingsSaved' ], 10, 2 );

		$settings = $this->getSettings();

		if ( is_admin() && $this->canManagePlugin() && $this->hasAnyDisableFlag( $settings ) ) {
			add_action( 'admin_notices', [ $this, 'showPersistentAdminNotice' ] );
			add_action( 'network_admin_notices', [ $this, 'showPersistentAdminNotice' ] );
		}

		if ( ! $this->hasAnyDisableFlag( $settings ) ) {
			return;
		}

		$this->detachUpdateChecks( $settings );

		if ( $this->needsCapabilityAndUpdateSummaryLayer() ) {
			add_filter( 'map_meta_cap', [ $this, 'blockUpdateMetaCaps' ], 999, 4 );
		}

		if ( ! empty( $settings['disable_core_updates'] ) ) {
			$this->disableCoreUpdates();
		}
		if ( ! empty( $settings['disable_plugin_updates'] ) ) {
			$this->disablePluginUpdates();
		}
		if ( ! empty( $settings['disable_theme_updates'] ) ) {
			$this->disableThemeUpdates();
		}
		if ( ! empty( $settings['disable_translation_updates'] ) ) {
			$this->disableTranslationUpdates( $settings );
		}

		if ( $this->needsCapabilityAndUpdateSummaryLayer() &&
			( ! empty( $settings['disable_core_updates'] ) ||
			! empty( $settings['disable_plugin_updates'] ) ||
			! empty( $settings['disable_theme_updates'] ) ) ) {
			$this->blockManualUpdateActions();
		}

		if ( $this->needsCapabilityAndUpdateSummaryLayer() ) {
			$this->removeUpdateNotifications( $settings );
		}

		$this->hideUpdateUI( $settings );

		if ( is_admin() ) {
			add_filter( 'site_status_tests', [ $this, 'filterSiteHealthTests' ], 999 );
			add_action( 'load-update-core.php', [ $this, 'registerUpdateScreenNotices' ] );
		}
	}

	/**
	 * @param array<string, mixed> $settings Raw module settings.
	 * @return array<string, bool>
	 */
	public static function sanitizeSettings( array $settings ): array {
		$out = [];
		foreach ( self::DEFAULT_SETTINGS as $key => $default ) {
			$out[ $key ] = ! empty( $settings[ $key ] );
		}

		return $out;
	}

	/**
	 * @param mixed                $settings  Module settings.
	 * @param string               $module_id Module ID.
	 * @param string               $context   `save` or `import`.
	 * @return mixed
	 */
	public static function filterSanitizeModuleSettings( $settings, string $module_id, string $context ) {
		unset( $context );

		if ( $module_id !== self::MODULE_ID || ! is_array( $settings ) ) {
			return $settings;
		}

		return self::sanitizeSettings( $settings );
	}

	/**
	 * @param string               $module_id Saved module ID.
	 * @param array<string, mixed> $settings  Saved settings.
	 */
	public static function handleSettingsSaved( string $module_id, array $settings ): void {
		if ( $module_id !== self::MODULE_ID ) {
			return;
		}

		self::syncUpdateCronSchedule( self::sanitizeSettings( $settings ) );
	}

	/**
	 * Clear or restore WordPress update cron events based on module settings.
	 *
	 * Called on settings save and module enable — not on every request.
	 *
	 * @param array<string, bool> $settings Sanitized module settings.
	 */
	public static function syncUpdateCronSchedule( array $settings ): void {
		self::loadUpdateApi();

		$settings = array_merge( self::DEFAULT_SETTINGS, $settings );

		if ( ! empty( $settings['disable_core_updates'] ) ) {
			wp_clear_scheduled_hook( 'wp_version_check' );
		} elseif ( ! wp_next_scheduled( 'wp_version_check' ) && ! wp_installing() ) {
			wp_schedule_event( time(), 'twicedaily', 'wp_version_check' );
		}

		if ( ! empty( $settings['disable_plugin_updates'] ) ) {
			wp_clear_scheduled_hook( 'wp_update_plugins' );
		} elseif ( ! wp_next_scheduled( 'wp_update_plugins' ) && ! wp_installing() ) {
			wp_schedule_event( time(), 'twicedaily', 'wp_update_plugins' );
		}

		if ( ! empty( $settings['disable_theme_updates'] ) ) {
			wp_clear_scheduled_hook( 'wp_update_themes' );
		} elseif ( ! wp_next_scheduled( 'wp_update_themes' ) && ! wp_installing() ) {
			wp_schedule_event( time(), 'twicedaily', 'wp_update_themes' );
		}
	}

	/**
	 * Restore all core WordPress update cron events (module disabled or uninstall).
	 */
	public static function restoreWordPressUpdateCronEvents(): void {
		self::loadUpdateApi();
		wp_schedule_update_checks();
	}

	/**
	 * @param array<string, bool> $settings Sanitized module settings.
	 * @return string[] Disabled type labels for display.
	 */
	public static function getDisabledTypeLabels( array $settings ): array {
		$map = [
			'disable_core_updates'        => __( 'Core', 'wp-powersuite' ),
			'disable_plugin_updates'      => __( 'Plugins', 'wp-powersuite' ),
			'disable_theme_updates'       => __( 'Themes', 'wp-powersuite' ),
			'disable_translation_updates' => __( 'Translations', 'wp-powersuite' ),
		];

		$labels = [];
		foreach ( $map as $key => $label ) {
			if ( ! empty( $settings[ $key ] ) ) {
				$labels[] = $label;
			}
		}

		return $labels;
	}

	private static function loadUpdateApi(): void {
		if ( ! function_exists( 'wp_schedule_update_checks' ) ) {
			require_once ABSPATH . 'wp-includes/update.php';
		}
	}

	private function hasAnyDisableFlag( array $settings ): bool {
		return ! empty( $settings['disable_core_updates'] ) ||
			! empty( $settings['disable_plugin_updates'] ) ||
			! empty( $settings['disable_theme_updates'] ) ||
			! empty( $settings['disable_translation_updates'] );
	}

	/**
	 * @param array<string, mixed> $tests Tests registry from {@see WP_Site_Health::get_tests()}.
	 * @return array<string, mixed>
	 */
	public function filterSiteHealthTests( array $tests ): array {
		$settings = $this->getSettings();

		if ( ! $this->hasAnyDisableFlag( $settings ) ) {
			return $tests;
		}

		$tests['direct']['wp_powersuite_disable_all_updates'] = [
			'label' => __( 'WP PowerSuite update controls', 'wp-powersuite' ),
			'test'  => [ $this, 'runSiteHealthUpdatesDisabledTest' ],
		];

		if ( ! empty( $settings['disable_core_updates'] ) && isset( $tests['async']['background_updates'] ) ) {
			unset( $tests['async']['background_updates'] );
		}

		if ( ( ! empty( $settings['disable_plugin_updates'] ) || ! empty( $settings['disable_theme_updates'] ) )
			&& isset( $tests['direct']['plugin_theme_auto_updates'] ) ) {
			unset( $tests['direct']['plugin_theme_auto_updates'] );
		}

		return $tests;
	}

	/**
	 * @return array<string, mixed>
	 */
	public function runSiteHealthUpdatesDisabledTest(): array {
		$settings = $this->getSettings();
		$labels   = self::getDisabledTypeLabels( $settings );
		$list     = implode( ', ', $labels );
		$core_off = ! empty( $settings['disable_core_updates'] );

		return [
			'label'       => __( 'WordPress updates paused by WP PowerSuite', 'wp-powersuite' ),
			'status'      => $core_off ? 'critical' : 'recommended',
			'badge'       => [
				'label' => __( 'Security', 'wp-powersuite' ),
				'color' => $core_off ? 'red' : 'orange',
			],
			'description' => sprintf(
				/* translators: %s: comma-separated list of disabled update types */
				__( 'Update types currently blocked: %s. Patch manually via deployment or hosting tools. WP-CLI updates are also blocked unless the wp_powersuite_disable_all_updates_allow_wp_cli filter is enabled. Re-enable types in WP PowerSuite → Disable All Updates.', 'wp-powersuite' ),
				$list
			),
			'actions'     => '',
			'test'        => 'wp_powersuite_disable_all_updates',
		];
	}

	/**
	 * Registers notice/footer hooks only on the Updates admin screen (avoids running callbacks on every admin page).
	 */
	public function registerUpdateScreenNotices(): void {
		add_action( 'admin_notices', [ $this, 'showUpdateDisabledNotice' ] );
		add_action( 'network_admin_notices', [ $this, 'showUpdateDisabledNotice' ] );
		add_action( 'update_footer', [ $this, 'addUpdateFooterBadge' ], 999 );
	}

	public function showPersistentAdminNotice(): void {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && in_array( $screen->id, [ 'update-core', 'update-core-network' ], true ) ) {
			return;
		}

		$labels = self::getDisabledTypeLabels( $this->getSettings() );
		if ( $labels === [] ) {
			return;
		}

		?>
		<div class="notice notice-warning">
			<p>
				<strong><?php esc_html_e( 'Updates paused by WP PowerSuite:', 'wp-powersuite' ); ?></strong>
				<?php
				echo esc_html(
					sprintf(
						/* translators: %s: comma-separated list of disabled update types */
						__( 'Blocked types: %s. Ensure you patch manually or adjust Disable All Updates module settings.', 'wp-powersuite' ),
						implode( ', ', $labels )
					)
				);
				?>
			</p>
		</div>
		<?php
	}

	private function detachUpdateChecks( array $settings ): void {
		if ( ! empty( $settings['disable_core_updates'] ) ) {
			remove_action( 'admin_init', '_maybe_update_core' );
			remove_action( 'wp_version_check', 'wp_version_check' );
		}

		if ( ! empty( $settings['disable_plugin_updates'] ) ) {
			remove_action( 'load-plugins.php', 'wp_update_plugins' );
			remove_action( 'load-update.php', 'wp_update_plugins' );
			remove_action( 'load-update-core.php', 'wp_update_plugins' );
			remove_action( 'admin_init', '_maybe_update_plugins' );
			remove_action( 'wp_update_plugins', 'wp_update_plugins' );
		}

		if ( ! empty( $settings['disable_theme_updates'] ) ) {
			remove_action( 'load-themes.php', 'wp_update_themes' );
			remove_action( 'load-update.php', 'wp_update_themes' );
			remove_action( 'load-update-core.php', 'wp_update_themes' );
			remove_action( 'admin_init', '_maybe_update_themes' );
			remove_action( 'wp_update_themes', 'wp_update_themes' );
		}

		if ( ! empty( $settings['disable_core_updates'] ) &&
			! empty( $settings['disable_plugin_updates'] ) &&
			! empty( $settings['disable_theme_updates'] ) ) {
			remove_action( 'wp_maybe_auto_update', 'wp_maybe_auto_update' );
		}
	}

	private function getSettings(): array {
		if ( $this->settings !== null ) {
			return $this->settings;
		}

		$stored         = $this->moduleSettings()->all( [] );
		$this->settings = self::sanitizeSettings( is_array( $stored ) ? $stored : [] );

		return $this->settings;
	}

	public function getDefaultSettings(): array {
		return self::DEFAULT_SETTINGS;
	}

	private function disableCoreUpdates(): void {
		add_filter( 'auto_update_core', '__return_false', 999 );
		add_filter( 'allow_major_auto_core_updates', '__return_false', 999 );
		add_filter( 'allow_minor_auto_core_updates', '__return_false', 999 );
		add_filter( 'allow_dev_auto_core_updates', '__return_false', 999 );
		add_filter( 'pre_site_transient_update_core', [ $this, 'filterPreUpdateCore' ], 999, 2 );
	}

	private function disablePluginUpdates(): void {
		add_filter( 'auto_update_plugin', '__return_false', 999 );
		add_filter( 'pre_site_transient_update_plugins', [ $this, 'filterPreUpdatePlugins' ], 999, 2 );
	}

	private function disableThemeUpdates(): void {
		add_filter( 'auto_update_theme', '__return_false', 999 );
		add_filter( 'pre_site_transient_update_themes', [ $this, 'filterPreUpdateThemes' ], 999, 2 );
	}

	private function disableTranslationUpdates( array $settings ): void {
		add_filter( 'auto_update_translation', '__return_false', 999 );

		if ( empty( $settings['disable_plugin_updates'] ) ) {
			add_filter( 'site_transient_update_plugins', [ $this, 'stripTranslationsFromTransient' ], 999 );
		}
		if ( empty( $settings['disable_theme_updates'] ) ) {
			add_filter( 'site_transient_update_themes', [ $this, 'stripTranslationsFromTransient' ], 999 );
		}
		if ( empty( $settings['disable_core_updates'] ) ) {
			add_filter( 'site_transient_update_core', [ $this, 'stripTranslationsFromTransient' ], 999 );
		}
	}

	public function filterPreUpdateCore( $pre, string $transient ): object {
		unset( $pre, $transient );

		$o                  = new stdClass();
		$o->last_checked    = time();
		$o->version_checked = wp_get_wp_version();
		$o->updates         = [];
		$o->translations    = [];

		return $o;
	}

	public function filterPreUpdatePlugins( $pre, string $transient ): object {
		unset( $pre, $transient );

		return $this->buildEmptyUpdateTransient( 'update_plugins' );
	}

	public function filterPreUpdateThemes( $pre, string $transient ): object {
		unset( $pre, $transient );

		return $this->buildEmptyUpdateTransient( 'update_themes' );
	}

	private function buildEmptyUpdateTransient( string $transient_name ): object {
		unset( $transient_name );

		$o               = new stdClass();
		$o->last_checked = time();
		$o->response     = [];
		$o->no_update    = [];
		$o->translations = [];
		$o->checked      = [];

		return $o;
	}

	/**
	 * @param mixed $value Transient value.
	 * @return mixed
	 */
	public function stripTranslationsFromTransient( $value ) {
		if ( ! is_object( $value ) ) {
			return $value;
		}
		if ( ! isset( $value->translations ) || empty( $value->translations ) ) {
			return $value;
		}

		$copy               = clone $value;
		$copy->translations = [];

		return $copy;
	}

	private function blockManualUpdateActions(): void {
		add_action( 'admin_init', [ $this, 'blockUpdateActions' ], 1 );
		add_action( 'wp_ajax_update-plugin', [ $this, 'blockAjaxUpdate' ], 1 );
		add_action( 'wp_ajax_update-theme', [ $this, 'blockAjaxUpdate' ], 1 );
		add_action( 'wp_ajax_update-core', [ $this, 'blockAjaxUpdate' ], 1 );
	}

	/**
	 * @param string[] $caps Primitive caps for the capability being checked.
	 * @return string[]
	 */
	public function blockUpdateMetaCaps( array $caps, string $cap, int $user_id, array $args ): array {
		unset( $user_id, $args );

		if ( ! isset( self::CAP_TO_SETTING[ $cap ] ) ) {
			return $caps;
		}

		$flag = self::CAP_TO_SETTING[ $cap ];
		if ( empty( $this->getSettings()[ $flag ] ) ) {
			return $caps;
		}

		return [ 'do_not_allow' ];
	}

	public function blockUpdateActions(): void {
		if ( ! isset( $_GET['action'] ) ) {
			return;
		}

		global $pagenow;
		if ( isset( $pagenow ) && $pagenow !== '' && ! in_array( $pagenow, [ 'update-core.php', 'update.php' ], true ) ) {
			return;
		}

		$settings = $this->getSettings();
		$action   = sanitize_text_field( wp_unslash( $_GET['action'] ) );
		$blocked  = [];

		if ( ! empty( $settings['disable_core_updates'] ) ) {
			$blocked[] = 'do-core-upgrade';
			$blocked[] = 'do-core-reinstall';
		}
		if ( ! empty( $settings['disable_plugin_updates'] ) ) {
			$blocked[] = 'upgrade-plugin';
			$blocked[] = 'update-selected';
		}
		if ( ! empty( $settings['disable_theme_updates'] ) ) {
			$blocked[] = 'upgrade-theme';
			$blocked[] = 'update-selected-themes';
		}

		if ( ! in_array( $action, $blocked, true ) ) {
			return;
		}

		$message = __( 'This type of update is disabled by WP PowerSuite. Please adjust the "Disable All Updates" module settings to enable this update type.', 'wp-powersuite' );
		wp_die(
			esc_html( $message ),
			esc_html__( 'Updates Disabled', 'wp-powersuite' ),
			[
				'response'  => 403,
				'back_link' => true,
			]
		);
	}

	public function blockAjaxUpdate(): void {
		if ( ! isset( $_POST['action'] ) ) {
			return;
		}

		$settings = $this->getSettings();
		$action   = sanitize_text_field( wp_unslash( $_POST['action'] ) );

		$blocked = false;
		if ( $action === 'update-core' && ! empty( $settings['disable_core_updates'] ) ) {
			$blocked = true;
		} elseif ( $action === 'update-plugin' && ! empty( $settings['disable_plugin_updates'] ) ) {
			$blocked = true;
		} elseif ( $action === 'update-theme' && ! empty( $settings['disable_theme_updates'] ) ) {
			$blocked = true;
		}

		if ( ! $blocked ) {
			return;
		}

		wp_send_json_error(
			[
				'message' => __( 'This type of update is disabled by WP PowerSuite. Please adjust the "Disable All Updates" module settings to enable this update type.', 'wp-powersuite' ),
			],
			403
		);
	}

	private function removeUpdateNotifications( array $settings ): void {
		if ( is_admin() && ! empty( $settings['disable_core_updates'] ) ) {
			remove_action( 'admin_notices', 'update_nag', 3 );
			remove_action( 'network_admin_notices', 'update_nag', 3 );
			remove_action( 'admin_notices', 'maintenance_nag' );
		}

		add_filter( 'wp_get_update_data', [ $this, 'filterUpdateData' ], 999 );
	}

	/**
	 * @param array{counts?: array<string, int>, title?: string} $update_data Update summary from core.
	 * @return array{counts: array<string, int>, title: string}
	 */
	public function filterUpdateData( array $update_data ): array {
		$settings = $this->getSettings();
		$counts   = isset( $update_data['counts'] ) && is_array( $update_data['counts'] ) ? $update_data['counts'] : [];

		if ( ! empty( $settings['disable_core_updates'] ) ) {
			$counts['wordpress'] = 0;
		}
		if ( ! empty( $settings['disable_plugin_updates'] ) ) {
			$counts['plugins'] = 0;
		}
		if ( ! empty( $settings['disable_theme_updates'] ) ) {
			$counts['themes'] = 0;
		}
		if ( ! empty( $settings['disable_translation_updates'] ) ) {
			$counts['translations'] = 0;
		}

		$counts['total'] = (int) ( $counts['plugins'] ?? 0 ) +
			(int) ( $counts['themes'] ?? 0 ) +
			(int) ( $counts['wordpress'] ?? 0 ) +
			(int) ( $counts['translations'] ?? 0 );

		$update_data['counts'] = $counts;
		$update_data['title']  = $update_data['title'] ?? '';

		return $update_data;
	}

	private function hideUpdateUI( array $settings ): void {
		if ( ! is_admin() ) {
			return;
		}

		if ( empty( $settings['disable_core_updates'] ) &&
			empty( $settings['disable_plugin_updates'] ) &&
			empty( $settings['disable_theme_updates'] ) ) {
			return;
		}

		add_action( 'admin_head', [ $this, 'hideUpdateUIElements' ] );
	}

	public function hideUpdateUIElements(): void {
		$settings = $this->getSettings();
		$rules    = [];

		if ( ! empty( $settings['disable_core_updates'] ) ) {
			$rules[] = '.update-nag';
		}
		if ( ! empty( $settings['disable_plugin_updates'] ) ) {
			$rules[] = '.plugins .update-message';
		}
		if ( ! empty( $settings['disable_theme_updates'] ) ) {
			$rules[] = '.themes .update-message';
		}

		if ( $rules === [] ) {
			return;
		}

		$selector = implode( ",\n\t\t\t", $rules );
		?>
		<style id="wp-powersuite-disable-updates-ui">
			<?php echo esc_html( $selector ); ?> {
				display: none !important;
			}
		</style>
		<?php
	}

	public function showUpdateDisabledNotice(): void {
		$screen = get_current_screen();
		if ( ! $screen || ! in_array( $screen->id, [ 'update-core', 'update-core-network' ], true ) ) {
			return;
		}

		$labels = self::getDisabledTypeLabels( $this->getSettings() );

		?>
		<div class="notice notice-warning">
			<p>
				<strong><?php esc_html_e( 'Updates Disabled:', 'wp-powersuite' ); ?></strong>
				<?php
				if ( $labels !== [] ) {
					echo esc_html(
						sprintf(
							/* translators: %s: comma-separated list of disabled update types */
							__( 'Blocked types: %s. Adjust the Disable All Updates module settings to re-enable any type.', 'wp-powersuite' ),
							implode( ', ', $labels )
						)
					);
				} else {
					esc_html_e( 'Some or all updates have been disabled by WP PowerSuite. Check the "Disable All Updates" module settings to manage which updates are disabled.', 'wp-powersuite' );
				}
				?>
			</p>
		</div>
		<?php
	}

	public function addUpdateFooterBadge( string $content ): string {
		$screen = get_current_screen();
		if ( ! $screen || ! in_array( $screen->id, [ 'update-core', 'update-core-network' ], true ) ) {
			return $content;
		}

		$content .= ' | <span class="wp-powersuite-disable-all-updates-footer-badge" style="color:#d63638;">' . esc_html__( 'Updates Disabled by WP PowerSuite', 'wp-powersuite' ) . '</span>';

		return $content;
	}

	/**
	 * Whether to attach meta-cap blocking and wp_get_update_data masking.
	 * Skips anonymous frontend and cron; keeps admin, AJAX, REST, and logged-in users who can update.
	 */
	private function needsCapabilityAndUpdateSummaryLayer(): bool {
		if ( wp_doing_cron() ) {
			return false;
		}

		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			/**
			 * Allow WP-CLI update commands when updates are otherwise blocked.
			 *
			 * @param bool $allow Default false.
			 */
			return ! (bool) apply_filters( 'wp_powersuite_disable_all_updates_allow_wp_cli', false );
		}

		if ( is_admin() ) {
			return true;
		}
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			return true;
		}
		if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
			return true;
		}
		if ( is_user_logged_in() ) {
			return current_user_can( 'update_core' )
				|| current_user_can( 'update_plugins' )
				|| current_user_can( 'update_themes' )
				|| current_user_can( 'update_languages' );
		}

		return false;
	}
}
