<?php
/**
 * Mersal Suite — remote releases, one-click install/update, announcements.
 *
 * Talks to the plain release folder on the Mersal site
 * (https://mersal.it/Wordpress-Plugins/<Folder>/Name-x.y.z.zip + manifest.json)
 * so the Suite can:
 *   - show REAL per-plugin update status on the Updates page,
 *   - install a missing Mersal plugin (or update an outdated one) in one click,
 *   - show the update-availability badge in the topbar,
 *   - surface a remotely managed announcement (announcement.json) on every tab.
 *
 * @package Mersal_Suite
 */

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

if ( class_exists( 'Mersal_Suite_Remote' ) ) {
	return;
}

/**
 * Remote release + announcement gateway for the Suite.
 */
class Mersal_Suite_Remote {

	const BASE         = 'https://mersal.it/Wordpress-Plugins';
	const VER_TTL      = 3 * HOUR_IN_SECONDS;
	const NEG_TTL      = 15 * MINUTE_IN_SECONDS;
	const NOTICE_TTL   = 6 * HOUR_IN_SECONDS;
	const DISMISS_META = 'mersal_suite_dismissed_notices';

	/** Per-plugin auto-update option names used by the shipped updaters. */
	const AUTO_OPTIONS = array(
		'motp_updater_auto_update',
		'mcamp_updater_auto_update',
		'mnotif_updater_auto_update',
	);

	/**
	 * Wire AJAX + toggle bridging. Idempotent.
	 */
	public static function init() {
		if ( has_action( 'wp_ajax_mersal_suite_install', array( __CLASS__, 'ajax_install' ) ) ) {
			return;
		}
		add_action( 'wp_ajax_mersal_suite_install', array( __CLASS__, 'ajax_install' ) );
		add_action( 'wp_ajax_mersal_suite_dismiss_notice', array( __CLASS__, 'ajax_dismiss_notice' ) );
		add_action( 'mersal_suite_toggled', array( __CLASS__, 'sync_auto_update' ), 10, 2 );
	}

	/**
	 * Release folder base URL on the Mersal site (no trailing slash).
	 *
	 * @return string
	 */
	public static function base_url() {
		return untrailingslashit( (string) apply_filters( 'mersal_suite_update_base', self::BASE ) );
	}

	/**
	 * Everything the Suite can install: the catalog plugins + extra family
	 * plugins that live in the same release folder (Mersal Assistant).
	 *
	 * @return array<string,array>
	 */
	public static function packages() {
		$out = array();
		if ( class_exists( 'Mersal_Suite_Catalog' ) ) {
			foreach ( Mersal_Suite_Catalog::ORDER as $id ) {
				$def = Mersal_Suite_Catalog::meta( $id );
				if ( $def && ! empty( $def['folder'] ) ) {
					$out[ $id ] = $def;
				}
			}
		}
		$out['as'] = array(
			'plugin_file' => 'mersal-assistant/mersal-assistant.php',
			'folder'      => 'Mersal-Assistant',
			'name'        => array( 'ar' => 'مساعد مرسال', 'en' => 'Mersal Assistant' ),
			'sub'         => array( 'ar' => 'مساعد ذكي لموقعك', 'en' => 'AI site assistant' ),
			'icon'        => 'smart_toy',
			'accent'      => '#5B5BD6',
		);
		/** Let hosts extend the installable family. */
		return (array) apply_filters( 'mersal_suite_packages', $out );
	}

	/* ------------------------------------------------------------------ */
	/* Remote version discovery                                           */
	/* ------------------------------------------------------------------ */

	/**
	 * Latest release info for a server folder (manifest.json, else listing).
	 *
	 * @param string $folder Server sub-folder (e.g. "Mersal-OTP").
	 * @param bool   $force  Skip the cache.
	 * @return array|WP_Error array{version,download_url,changelog?,last_updated?}
	 */
	public static function remote( $folder, $force = false ) {
		$key = 'msuite_rv_' . md5( $folder );
		if ( ! $force ) {
			$cached = get_transient( $key );
			if ( false !== $cached ) {
				return $cached;
			}
		}

		$base = self::base_url() . '/' . rawurlencode( $folder );
		$out  = array();

		$resp = wp_remote_get( $base . '/manifest.json', array( 'timeout' => 10, 'headers' => array( 'Accept' => 'application/json' ) ) );
		if ( ! is_wp_error( $resp ) && 200 === (int) wp_remote_retrieve_response_code( $resp ) ) {
			$data = json_decode( wp_remote_retrieve_body( $resp ), true );
			if ( is_array( $data ) && ! empty( $data['version'] ) ) {
				$out['version']      = sanitize_text_field( $data['version'] );
				$out['download_url'] = ! empty( $data['download_url'] )
					? esc_url_raw( $data['download_url'] )
					: $base . '/' . rawurlencode( $folder . '-' . $out['version'] . '.zip' );
				foreach ( array( 'changelog', 'last_updated' ) as $k ) {
					if ( ! empty( $data[ $k ] ) && is_string( $data[ $k ] ) ) {
						$out[ $k ] = wp_kses_post( $data[ $k ] );
					}
				}
			}
		}

		if ( empty( $out['version'] ) ) {
			// Fall back to the autoindex listing: "Folder-x.y.z.zip", highest wins.
			$resp = wp_remote_get( $base . '/', array( 'timeout' => 10 ) );
			if ( is_wp_error( $resp ) ) {
				set_transient( $key, $resp, self::NEG_TTL );
				return $resp;
			}
			$body   = wp_remote_retrieve_body( $resp );
			$prefix = preg_quote( $folder, '/' );
			if ( 200 !== (int) wp_remote_retrieve_response_code( $resp )
				|| ! preg_match_all( '/(' . $prefix . '-([0-9]+(?:\.[0-9]+)+))\.zip/i', $body, $m, PREG_SET_ORDER ) ) {
				$err = new WP_Error( 'msuite_no_release', sprintf( 'No release found for "%s".', $folder ) );
				set_transient( $key, $err, self::NEG_TTL );
				return $err;
			}
			$best    = '0.0.0';
			$best_fn = '';
			foreach ( $m as $hit ) {
				if ( version_compare( $hit[2], $best, '>' ) ) {
					$best    = $hit[2];
					$best_fn = $hit[0];
				}
			}
			$out = array(
				'version'      => $best,
				'download_url' => $base . '/' . rawurlencode( $best_fn ),
			);
		}

		set_transient( $key, $out, self::VER_TTL );
		return $out;
	}

	/**
	 * Installed version of a plugin file, or null.
	 *
	 * @param string $plugin_file Plugin basename.
	 * @return string|null
	 */
	public static function installed_version( $plugin_file ) {
		$path = WP_PLUGIN_DIR . '/' . $plugin_file;
		if ( ! is_readable( $path ) ) {
			return null;
		}
		if ( ! function_exists( 'get_plugin_data' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$data = get_plugin_data( $path, false, false );
		return ! empty( $data['Version'] ) ? $data['Version'] : '0.0.0';
	}

	/**
	 * Status rows for every installable package.
	 *
	 * @param bool $force Skip remote caches.
	 * @return array<int,array>
	 */
	public static function snapshot( $force = false ) {
		if ( ! function_exists( 'is_plugin_active' ) ) {
			require_once ABSPATH . 'wp-admin/includes/plugin.php';
		}
		$rows = array();
		foreach ( self::packages() as $id => $def ) {
			$installed = self::installed_version( $def['plugin_file'] );
			$remote    = self::remote( $def['folder'], $force );
			$r_ver     = is_wp_error( $remote ) ? null : $remote['version'];

			if ( null === $installed ) {
				$status = $r_ver ? 'missing' : 'error';
			} elseif ( ! $r_ver ) {
				$status = 'error';
			} elseif ( version_compare( $installed, $r_ver, '<' ) ) {
				$status = 'outdated';
			} elseif ( ! is_plugin_active( $def['plugin_file'] ) ) {
				$status = 'inactive';
			} else {
				$status = 'latest';
			}

			$rows[] = array(
				'id'        => $id,
				'def'       => $def,
				'installed' => $installed,
				'remote'    => $r_ver,
				'changelog' => ( ! is_wp_error( $remote ) && ! empty( $remote['changelog'] ) ) ? $remote['changelog'] : '',
				'status'    => $status,
				'error'     => is_wp_error( $remote ) ? $remote->get_error_message() : '',
			);
		}
		return $rows;
	}

	/**
	 * Number of installed Mersal plugins with a newer release available.
	 *
	 * @return int
	 */
	public static function updates_count() {
		$n = 0;
		foreach ( self::snapshot() as $row ) {
			if ( 'outdated' === $row['status'] ) {
				$n++;
			}
		}
		return $n;
	}

	/* ------------------------------------------------------------------ */
	/* One-click install / update                                         */
	/* ------------------------------------------------------------------ */

	/**
	 * AJAX: install (or update) a Mersal plugin from the release folder.
	 */
	public static function ajax_install() {
		check_ajax_referer( 'mersal_suite_install', 'nonce' );

		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above.
		$id   = isset( $_POST['id'] ) ? sanitize_key( wp_unslash( $_POST['id'] ) ) : '';
		$pkgs = self::packages();
		if ( ! isset( $pkgs[ $id ] ) ) {
			wp_send_json_error( array( 'message' => 'unknown plugin' ), 400 );
		}
		$def       = $pkgs[ $id ];
		$installed = self::installed_version( $def['plugin_file'] );
		$cap       = null === $installed ? 'install_plugins' : 'update_plugins';
		if ( ! current_user_can( $cap ) ) {
			wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
		}

		$remote = self::remote( $def['folder'], true );
		if ( is_wp_error( $remote ) || empty( $remote['download_url'] ) ) {
			wp_send_json_error( array( 'message' => is_wp_error( $remote ) ? $remote->get_error_message() : 'no download url' ) );
		}

		// Only ever install over HTTPS from the trusted Mersal host.
		$allowed = wp_parse_url( self::base_url(), PHP_URL_HOST );
		$host    = wp_parse_url( $remote['download_url'], PHP_URL_HOST );
		$scheme  = wp_parse_url( $remote['download_url'], PHP_URL_SCHEME );
		if ( ! $allowed || $host !== $allowed || 'https' !== strtolower( (string) $scheme ) ) {
			wp_send_json_error( array( 'message' => 'untrusted download host' ), 400 );
		}

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

		// Safety net: make sure the extracted folder matches the plugin dir even
		// if a ZIP was built with a versioned top folder.
		$desired = dirname( $def['plugin_file'] );
		$fix     = function ( $source, $remote_source ) use ( $desired ) {
			global $wp_filesystem;
			if ( ! $desired || '.' === $desired ) {
				return $source;
			}
			$corrected = trailingslashit( $remote_source ) . $desired . '/';
			if ( trailingslashit( $source ) === $corrected ) {
				return $source;
			}
			if ( $wp_filesystem && $wp_filesystem->move( untrailingslashit( $source ), untrailingslashit( $corrected ), true ) ) {
				return $corrected;
			}
			return $source;
		};
		add_filter( 'upgrader_source_selection', $fix, 9, 2 );

		$skin     = new WP_Ajax_Upgrader_Skin();
		$upgrader = new Plugin_Upgrader( $skin );

		if ( null === $installed ) {
			$result = $upgrader->install( $remote['download_url'] );
		} else {
			// Prime the update transient with the fresh package so the upgrader
			// never reuses a stale URL from an earlier check.
			$transient = get_site_transient( 'update_plugins' );
			if ( ! is_object( $transient ) ) {
				$transient = new stdClass();
			}
			if ( ! isset( $transient->response ) || ! is_array( $transient->response ) ) {
				$transient->response = array();
			}
			$transient->response[ $def['plugin_file'] ] = (object) array(
				'id'          => $def['plugin_file'],
				'slug'        => $desired,
				'plugin'      => $def['plugin_file'],
				'new_version' => $remote['version'],
				'package'     => $remote['download_url'],
				'url'         => 'https://mersal.it',
			);
			set_site_transient( 'update_plugins', $transient );
			$result = $upgrader->upgrade( $def['plugin_file'] );
		}

		remove_filter( 'upgrader_source_selection', $fix, 9 );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error( array( 'message' => $result->get_error_message() ) );
		}
		if ( false === $result || null === $result ) {
			$errors = $skin->get_errors();
			$msg    = ( is_wp_error( $errors ) && $errors->has_errors() ) ? $errors->get_error_message() : 'installation failed';
			wp_send_json_error( array( 'message' => $msg ) );
		}

		if ( ! is_plugin_active( $def['plugin_file'] ) ) {
			$act = activate_plugin( $def['plugin_file'] );
			if ( is_wp_error( $act ) ) {
				wp_send_json_error( array( 'message' => $act->get_error_message() ) );
			}
		}

		delete_transient( 'msuite_rv_' . md5( $def['folder'] ) );
		delete_site_transient( 'update_plugins' );
		wp_send_json_success( array( 'version' => $remote['version'] ) );
	}

	/**
	 * Keep the real per-plugin auto-update options in sync with the Suite toggle.
	 *
	 * @param string $key Toggle key.
	 * @param bool   $on  New state.
	 */
	public static function sync_auto_update( $key, $on ) {
		if ( 'autoUpdate' !== $key ) {
			return;
		}
		foreach ( self::AUTO_OPTIONS as $opt ) {
			update_option( $opt, $on ? '1' : '0' );
		}
	}

	/**
	 * Whether Mersal auto-updates are on (any shipped updater option wins).
	 *
	 * @return bool
	 */
	public static function auto_update_on() {
		foreach ( self::AUTO_OPTIONS as $opt ) {
			if ( '1' === get_option( $opt, '' ) ) {
				return true;
			}
		}
		return false;
	}

	/* ------------------------------------------------------------------ */
	/* Remote announcement (announcement.json on the Mersal site)         */
	/* ------------------------------------------------------------------ */

	/**
	 * The active announcement for this user, or null.
	 *
	 * announcement.json shape:
	 * { "id":"2026-07-a", "enabled":true, "tone":"accent", "icon":"campaign",
	 *   "title":{"ar":"..","en":".."}, "body":{"ar":"..","en":".."},
	 *   "url":"https://..", "url_label":{"ar":"..","en":".."},
	 *   "expires":"2026-08-01" }
	 *
	 * @return array|null
	 */
	public static function announcement() {
		$data = get_transient( 'msuite_notice' );
		if ( false === $data ) {
			$resp = wp_remote_get( self::base_url() . '/announcement.json', array( 'timeout' => 8, 'headers' => array( 'Accept' => 'application/json' ) ) );
			$data = array();
			if ( ! is_wp_error( $resp ) && 200 === (int) wp_remote_retrieve_response_code( $resp ) ) {
				$json = json_decode( wp_remote_retrieve_body( $resp ), true );
				if ( is_array( $json ) ) {
					$data = $json;
				}
			}
			set_transient( 'msuite_notice', $data, empty( $data ) ? self::NEG_TTL : self::NOTICE_TTL );
		}

		if ( empty( $data ) || ! is_array( $data ) || empty( $data['enabled'] ) || empty( $data['id'] ) || empty( $data['title'] ) ) {
			return null;
		}
		if ( ! empty( $data['expires'] ) && strtotime( (string) $data['expires'] . ' 23:59:59' ) < time() ) {
			return null;
		}
		$dismissed = get_user_meta( get_current_user_id(), self::DISMISS_META, true );
		if ( is_array( $dismissed ) && in_array( (string) $data['id'], $dismissed, true ) ) {
			return null;
		}
		return $data;
	}

	/**
	 * AJAX: remember that this user dismissed an announcement.
	 */
	public static function ajax_dismiss_notice() {
		check_ajax_referer( 'mersal_suite_pref', 'nonce' );
		if ( ! current_user_can( 'manage_options' ) ) {
			wp_send_json_error( array( 'message' => 'forbidden' ), 403 );
		}
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- verified above.
		$id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
		if ( '' === $id ) {
			wp_send_json_error( array( 'message' => 'bad id' ), 400 );
		}
		$list = get_user_meta( get_current_user_id(), self::DISMISS_META, true );
		$list = is_array( $list ) ? $list : array();
		if ( ! in_array( $id, $list, true ) ) {
			$list[] = $id;
			// Keep the meta small — only the last 20 dismissals matter.
			$list = array_slice( $list, -20 );
			update_user_meta( get_current_user_id(), self::DISMISS_META, $list );
		}
		wp_send_json_success();
	}
}
