<?php

namespace WPDRMS\ASP\Misc;

use WP_Error;
use WP_REST_Request;
use WP_REST_Server;
use WPDRMS\ASP\Utils\Plugin;
use WPDRMS\PluginCore\Interfaces\Registrable;

if ( !defined('ABSPATH') ) {
	die('-1');
}

/**
 * Adjusts the plugin's admin REST API for demo mode, in two complementary ways.
 *
 * When the site runs in demo mode (`ASP_DEMO`) the admin menu is exposed to any
 * logged-in user (`read` capability, see WPDRMS\ASP\Core\Menu) so visitors can
 * explore the panels. Those non-admin visitors:
 *
 *  1. **must be able to read** every panel — but the admin REST routes guard
 *     themselves with `AbstractRest::allowOnlyAdmins()` (requires `manage_options`),
 *     which would 401 every GET. {@see allowDemoRestReads} hooks the shared
 *     `wpdrms/core/rest/allow_only_admins` / `..._logged_in` filters to grant
 *     read-only (GET/HEAD/OPTIONS) access while the demo lock is active. This is
 *     namespace-agnostic, so it also unblocks the shared admin-ui lookup routes
 *     (posts/users/taxonomies/…) registered under `wpdrms_plugin_core`.
 *
 *  2. **must not be able to persist anything**. Every modern admin panel saves
 *     through this plugin's REST namespace (`ASP_DIR`), so a single
 *     `rest_pre_dispatch` guard ({@see blockDemoWrites}) blocks all write requests
 *     centrally. Because the read filter only ever loosens read-only methods, the
 *     `allowOnlyAdmins()` capability check still rejects demo writes too — so even
 *     a future write route registered outside the `ASP_DIR` namespace stays locked.
 *
 * Not affected by the write guard:
 *  - real administrators / super-admins (they satisfy `manage_options`);
 *  - read-only requests (GET / HEAD / OPTIONS), so panels can still be viewed;
 *  - the public front-end search API ({@see \WPDRMS\ASP\Api\Rest0\Rest}), which is
 *    GET-only;
 *  - the `search_instance` endpoints — the per-instance options are the one part
 *    of the admin UI demo visitors are allowed to change. The legacy instance
 *    list and per-instance options editor are plain admin-page POST handlers
 *    (backend/main_settings.php, backend/search.php), not REST, so they are
 *    unaffected too.
 */
class DemoMode implements Registrable {
	/**
	 * HTTP methods that only read state and are therefore safe for demo visitors.
	 */
	private const READ_ONLY_METHODS = array( 'GET', 'HEAD', 'OPTIONS' );

	public function register(): void {
		add_filter('rest_pre_dispatch', array( $this, 'blockDemoWrites' ), 10, 3);
		add_filter('wpdrms/core/rest/allow_only_admins', array( $this, 'allowDemoRestReads' ), 10, 2);
		add_filter('wpdrms/core/rest/allow_only_logged_in', array( $this, 'allowDemoRestReads' ), 10, 2);
	}

	public function deregister(): void {
		remove_filter('rest_pre_dispatch', array( $this, 'blockDemoWrites' ), 10);
		remove_filter('wpdrms/core/rest/allow_only_admins', array( $this, 'allowDemoRestReads' ), 10);
		remove_filter('wpdrms/core/rest/allow_only_logged_in', array( $this, 'allowDemoRestReads' ), 10);
	}

	/**
	 * Grants read-only REST access to demo visitors who would otherwise be rejected by
	 * the admin/logged-in permission callbacks.
	 *
	 * Loosen-only: this only ever returns true (to grant access); it preserves an allow
	 * another handler already produced and never tightens. Access is granted only for
	 * read-only methods while the demo lock is active, so writes still fall through to
	 * the normal capability check (and are blocked there as well as by {@see blockDemoWrites}).
	 *
	 * @param bool                 $allowed Whether access has already been granted.
	 * @param WP_REST_Request|null $request The current request.
	 * @return bool
	 */
	public function allowDemoRestReads( $allowed, $request ): bool {
		if ( $allowed ) {
			return true;
		}

		if ( ! Plugin::isDemoWriteLocked() ) {
			return false;
		}

		return $request instanceof WP_REST_Request && $this->isReadOnly( $request );
	}

	/**
	 * Short-circuits write requests to the plugin's admin REST namespace while the
	 * demo write lock is active.
	 *
	 * @param mixed           $result  Response to replace the requested version with. Null by default.
	 * @param WP_REST_Server  $server  Server instance (unused).
	 * @param WP_REST_Request $request Current request.
	 * @return mixed
	 */
	public function blockDemoWrites( $result, WP_REST_Server $server, WP_REST_Request $request ) {
		// Respect a short-circuit already set by another handler.
		if ( null !== $result ) {
			return $result;
		}

		if ( ! Plugin::isDemoWriteLocked() ) {
			return $result;
		}

		// Read-only methods are always allowed so the panels can still be viewed.
		if ( $this->isReadOnly( $request ) ) {
			return $result;
		}

		$route = $request->get_route();

		// Only guard this plugin's own admin routes (registered under ASP_DIR).
		if ( 0 !== strpos( $route, '/' . ASP_DIR . '/' ) ) {
			return $result;
		}

		// The per-instance search options are the one admin area demo users may edit.
		if ( false !== strpos( $route, 'search_instance' ) ) {
			return $result;
		}

		return new WP_Error(
			'asp_demo_read_only',
			__( 'Settings cannot be changed while the plugin is running in demo mode.', 'ajax-search-pro' ),
			array( 'status' => 403 )
		);
	}

	/**
	 * Whether the request only reads state (safe to expose to demo visitors).
	 *
	 * @param WP_REST_Request $request The current request.
	 * @return bool
	 */
	private function isReadOnly( WP_REST_Request $request ): bool {
		return in_array( $request->get_method(), self::READ_ONLY_METHODS, true );
	}
}
