<?php
/**
 * Live preview REST route.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse\Rest;

use WPXero\MenuVerse\Modules\Module;

defined( 'ABSPATH' ) || exit;

/**
 * Renders a module with unsaved settings for the dashboard preview pane.
 *
 * The response is markup plus asset URLs; the dashboard assembles them into an
 * iframe document. Nothing here is persisted and no user supplied code is ever
 * executed, so the preview cannot become a second, weaker write path.
 */
final class Preview_Controller extends Rest_Controller {

	/**
	 * Registers routes.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			self::REST_NAMESPACE,
			'/preview',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'render_preview' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						'module'   => array(
							'type'     => 'string',
							'required' => true,
							'pattern'  => '^[a-z][a-z0-9_]*$',
						),
						'settings' => array(
							'type'     => 'object',
							'required' => false,
							'default'  => array(),
						),
						'viewport' => array(
							'type'    => 'integer',
							'default' => 1280,
							'minimum' => 240,
							'maximum' => 3840,
						),
					),
				),
			)
		);
	}

	/**
	 * Renders a module preview.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function render_preview( \WP_REST_Request $request ) {
		$module_id = (string) $request->get_param( 'module' );
		$registry  = $this->plugin()->modules();

		if ( ! $registry->has( $module_id ) ) {
			return new \WP_Error(
				'menuverse_unknown_module',
				__( 'Unknown module.', 'menuverse' ),
				array( 'status' => 404 )
			);
		}

		$module = $registry->get( $module_id );

		if ( ! $module instanceof Module ) {
			return new \WP_Error(
				'menuverse_module_unavailable',
				__( 'That module cannot be previewed.', 'menuverse' ),
				array( 'status' => 500 )
			);
		}

		// Preview must work before a module is switched on, so assets are
		// registered here rather than relying on the frontend boot path.
		$this->plugin()->assets()->register_base();
		$module->register_assets();

		$before_styles  = $this->queued( 'styles' );
		$before_scripts = $this->queued( 'scripts' );

		$settings = $this->plugin()->settings()->sanitize_module(
			$module_id,
			(array) $request->get_param( 'settings' )
		);

		if ( is_wp_error( $settings ) ) {
			return $settings;
		}

		$markup = $module->output(
			$settings,
			array(
				'source'   => 'preview',
				'viewport' => (int) $request->get_param( 'viewport' ),
			)
		);

		$styles  = $this->expand( 'styles', array_diff( $this->queued( 'styles' ), $before_styles ) );
		$scripts = $this->expand( 'scripts', array_diff( $this->queued( 'scripts' ), $before_scripts ) );

		return rest_ensure_response(
			array(
				'module'       => $module_id,
				'html'         => $markup,
				'styles'       => $this->urls( 'styles', $styles ),
				'scripts'      => $this->urls( 'scripts', $scripts ),
				'inlineStyles' => $this->inline_styles( $styles ),
			)
		);
	}

	/**
	 * Returns the currently queued handles for a dependency type.
	 *
	 * @param string $type Either styles or scripts.
	 * @return string[]
	 */
	private function queued( $type ) {
		$registry = 'styles' === $type ? wp_styles() : wp_scripts();

		return array_values( (array) $registry->queue );
	}

	/**
	 * Collects the inline CSS attached to the resolved style handles.
	 *
	 * Token declarations are added with wp_add_inline_style, so without this
	 * the preview would render the module without its theming.
	 *
	 * @param string[] $handles Style handles.
	 * @return string[]
	 */
	private function inline_styles( array $handles ) {
		$styles = wp_styles();
		$inline = array();

		foreach ( $handles as $handle ) {
			$data = $styles->get_data( $handle, 'after' );

			if ( ! empty( $data ) ) {
				$inline[] = implode( "\n", (array) $data );
			}
		}

		return $inline;
	}

	/**
	 * Expands handles to include their registered dependencies, in load order.
	 *
	 * WordPress resolves dependencies when it prints a queue, so the queue
	 * alone would miss the shared token stylesheet the preview needs.
	 *
	 * @param string   $type    Either styles or scripts.
	 * @param string[] $handles Handles to expand.
	 * @return string[]
	 */
	private function expand( $type, array $handles ) {
		$registry = 'styles' === $type ? wp_styles() : wp_scripts();
		$resolved = array();

		$walk = function ( $handle ) use ( &$walk, &$resolved, $registry ) {
			if ( in_array( $handle, $resolved, true ) || ! isset( $registry->registered[ $handle ] ) ) {
				return;
			}

			foreach ( (array) $registry->registered[ $handle ]->deps as $dep ) {
				$walk( $dep );
			}

			$resolved[] = $handle;
		};

		foreach ( $handles as $handle ) {
			$walk( $handle );
		}

		return $resolved;
	}

	/**
	 * Resolves handles to absolute URLs.
	 *
	 * @param string   $type    Either styles or scripts.
	 * @param string[] $handles Handles to resolve.
	 * @return string[]
	 */
	private function urls( $type, array $handles ) {
		$registry = 'styles' === $type ? wp_styles() : wp_scripts();
		$urls     = array();

		foreach ( $handles as $handle ) {
			if ( ! isset( $registry->registered[ $handle ] ) ) {
				continue;
			}

			$src = $registry->registered[ $handle ]->src;

			if ( ! $src ) {
				continue;
			}

			$version = $registry->registered[ $handle ]->ver;

			if ( 0 !== strpos( $src, 'http' ) && 0 !== strpos( $src, '//' ) ) {
				$src = site_url( $src );
			}

			$urls[] = esc_url_raw( $version ? add_query_arg( 'ver', $version, $src ) : $src );
		}

		return $urls;
	}
}
