<?php

namespace DisableRESTAPI;

use WP_PowerSuite\Core\AbstractModule;

class Module extends AbstractModule {

	private const MODULE_ID = 'disable-rest-api';
	private const MODULE_VERSION = '1.2.0';

	public const MODE_PUBLIC     = 'disable_public';
	public const MODE_COMPLETELY = 'disable_completely';

	/** Late priority so other plugins cannot re-enable REST after this module. */
	private const FILTER_PRIORITY_LAST = \PHP_INT_MAX;

	private ?array $settings_cache = null;

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

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

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

	public function getDescription(): string {
		return __( 'Block anonymous access to WordPress core REST endpoints (users, settings, themes, and similar) while leaving logged-in staff and third-party plugin REST routes untouched.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		// REST auth + discovery hooks only; skip cron (no REST/HTML output there).
		return [ 'admin', 'frontend', 'ajax', 'rest' ];
	}

	protected function registerHooks(): void {
		$settings = $this->getSettings();

		if ( ! empty( $settings['remove_link_from_head'] ) ) {
			remove_action( 'xmlrpc_rsd_apis', 'rest_output_rsd' );
			if ( ! is_admin() ) {
				remove_action( 'wp_head', 'rest_output_link_wp_head', 10 );
				remove_action( 'template_redirect', 'rest_output_link_header', 11, 0 );
			}
		}

		if ( did_action( 'rest_api_init' ) ) {
			$this->registerRestAuthentication();
			return;
		}

		add_action( 'rest_api_init', [ $this, 'registerRestAuthentication' ], 1 );
	}

	/**
	 * Runs only for REST requests. Registers rest_authentication_errors without touching non-REST traffic.
	 */
	public function registerRestAuthentication(): void {
		static $registered = false;
		if ( $registered ) {
			return;
		}
		$registered = true;

		$settings = $this->getSettings();
		$callback = self::MODE_COMPLETELY === $settings['mode']
			? [ $this, 'disableRESTAPICompletely' ]
			: [ $this, 'restrictRESTAPIAccess' ];

		add_filter( 'rest_authentication_errors', $callback, self::FILTER_PRIORITY_LAST );
	}

	/**
	 * @return array{mode: string, remove_link_from_head: bool}
	 */
	public static function defaultSettings(): array {
		return [
			'mode'                  => self::MODE_PUBLIC,
			'remove_link_from_head' => true,
		];
	}

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

	/**
	 * @param array<string, mixed> $settings
	 * @return array{mode: string, remove_link_from_head: bool}
	 */
	public static function normalizeSettings( array $settings ): array {
		$out = array_merge( self::defaultSettings(), $settings );

		if ( ! in_array( $out['mode'], [ self::MODE_PUBLIC, self::MODE_COMPLETELY ], true ) ) {
			$out['mode'] = self::MODE_PUBLIC;
		}

		$out['remove_link_from_head'] = ! empty( $out['remove_link_from_head'] );

		return $out;
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array{mode: string, remove_link_from_head: bool}
	 */
	public static function sanitizeSettings( array $settings ): array {
		return self::normalizeSettings( $settings );
	}

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

		$saved = $this->moduleSettings()->all( [] );
		$out   = self::normalizeSettings( is_array( $saved ) ? $saved : [] );

		$this->settings_cache = $out;

		return $this->settings_cache;
	}

	public function restrictRESTAPIAccess( $result ) {
		// Preserve WP_Error, and "true" (auth already handled, e.g. Application Passwords). Only act on null.
		if ( null !== $result ) {
			return $result;
		}

		if ( is_user_logged_in() ) {
			return $result;
		}

		$context = $this->getRestRequestContext();
		if ( ! $this->shouldBlockAnonymousCoreRest( $context['route'], $context['path'] ) ) {
			return $result;
		}

		return new \WP_Error(
			'rest_cannot_access',
			__( 'Only authenticated users can access the WordPress REST API.', 'wp-powersuite' ),
			[ 'status' => rest_authorization_required_code() ]
		);
	}

	public function disableRESTAPICompletely( $result ) {
		if ( is_wp_error( $result ) ) {
			return $result;
		}

		$context = $this->getRestRequestContext();

		if ( $this->isRouteAllowedWhenCompletelyDisabled( $context['route'], $context['path'] ) ) {
			return $result;
		}

		return new \WP_Error(
			'rest_disabled',
			__( 'REST API is disabled.', 'wp-powersuite' ),
			[ 'status' => 403 ]
		);
	}

	/**
	 * @return array{route: string, path: string}
	 */
	private function getRestRequestContext(): array {
		$route = '';
		if ( function_exists( 'rest_get_request' ) ) {
			$req = rest_get_request();
			if ( $req instanceof \WP_REST_Request ) {
				$route = (string) $req->get_route();
			}
		}

		if ( '' === $route && isset( $_GET['rest_route'] ) ) {
			$route = (string) wp_unslash( $_GET['rest_route'] );
		}

		$path = '';
		if ( isset( $_SERVER['REQUEST_URI'] ) ) {
			$uri  = (string) wp_unslash( $_SERVER['REQUEST_URI'] );
			$path = (string) wp_parse_url( $uri, PHP_URL_PATH );
		}

		if ( '' === $route && '' !== $path ) {
			$route = $this->routeFromRestPath( $path );
		}

		return [
			'route' => $route,
			'path'  => is_string( $path ) ? $path : '',
		];
	}

	private function routeFromRestPath( string $path ): string {
		if ( ! function_exists( 'rest_get_url_prefix' ) ) {
			return '';
		}

		$prefix = rest_get_url_prefix();
		if ( '' === $prefix ) {
			return '';
		}

		$needle = '/' . trim( $prefix, '/' ) . '/';
		$pos    = strpos( $path, $needle );
		if ( false === $pos ) {
			if ( str_ends_with( $path, '/' . trim( $prefix, '/' ) ) ) {
				return '/';
			}
			return '';
		}

		$suffix = substr( $path, $pos + strlen( $needle ) - 1 );
		return '/' . ltrim( (string) $suffix, '/' );
	}

	private function shouldBlockAnonymousCoreRest( string $route, string $path ): bool {
		$is_core = $this->isWordPressCoreRestRoute( $route );

		/**
		 * Filter whether anonymous access to a WordPress core REST route should be blocked.
		 *
		 * @param bool   $block Default true when the route belongs to a core REST namespace.
		 * @param string $route REST route (e.g. /wp/v2/users).
		 * @param string $path  URL path (e.g. /wp-json/wp/v2/users).
		 */
		return (bool) apply_filters( 'wpps_disable_rest_api_block_anonymous_core', $is_core, $route, $path );
	}

	private function isWordPressCoreRestRoute( string $route ): bool {
		if ( '' === $route || '/' === $route ) {
			return true;
		}

		$route = '/' . ltrim( $route, '/' );

		foreach ( $this->getCoreRestNamespaces() as $namespace ) {
			$namespace = trim( (string) $namespace, '/' );
			if ( '' === $namespace ) {
				continue;
			}

			if ( $route === '/' . $namespace || str_starts_with( $route, '/' . $namespace . '/' ) ) {
				return true;
			}
		}

		// Future core namespaces such as wp/v3 or wp-new-feature/v1.
		if ( preg_match( '#^/wp(/|-)#', $route ) ) {
			return true;
		}

		return false;
	}

	/**
	 * WordPress core REST namespaces blocked for anonymous users in public mode.
	 *
	 * @return list<string>
	 */
	private function getCoreRestNamespaces(): array {
		$namespaces = [
			'wp/v2',
			'wp-site-health/v1',
			'wp-block-editor/v1',
			'wp-abilities/v1',
			'oembed/1.0',
		];

		/**
		 * Core REST namespaces treated as WordPress-owned in public mode.
		 *
		 * @param list<string> $namespaces Namespace slugs (no leading slash).
		 */
		return apply_filters( 'wpps_disable_rest_api_core_namespaces', $namespaces );
	}

	private function isRouteAllowedWhenCompletelyDisabled( string $route, string $path ): bool {
		/**
		 * Allow specific REST routes to bypass complete REST disable (for client plugins/themes).
		 *
		 * @param bool   $allow Default false — block unless an integration returns true.
		 * @param string $route REST route (e.g. /wc/store/v1/cart).
		 * @param string $path  URL path (e.g. /wp-json/wc/store/v1/cart).
		 */
		$allow = (bool) apply_filters( 'wpps_disable_rest_api_allow_route', false, $route, $path );

		if ( $allow ) {
			return true;
		}

		/**
		 * @deprecated 1.2.0 Use {@see 'wpps_disable_rest_api_allow_route'} instead.
		 */
		return (bool) apply_filters( 'wpps_disable_rest_api_allow_unauthenticated', false, $route, $path );
	}

}
