<?php
/**
 * Import, export and dashboard state routes.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse\Rest;

defined( 'ABSPATH' ) || exit;

/**
 * Moves settings in and out of the site as JSON, and stores dashboard state.
 */
final class Tools_Controller extends Rest_Controller {

	/**
	 * Marker identifying a MenuVerse export file.
	 *
	 * @var string
	 */
	const FILE_TYPE = 'menuverse-settings';

	/**
	 * Registers routes.
	 *
	 * @return void
	 */
	public function register_routes() {
		register_rest_route(
			self::REST_NAMESPACE,
			'/export',
			array(
				array(
					'methods'             => \WP_REST_Server::READABLE,
					'callback'            => array( $this, 'export' ),
					'permission_callback' => array( $this, 'permission_check' ),
				),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/import',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'import' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						'payload' => array(
							'type'     => 'object',
							'required' => true,
						),
						'dry_run' => array(
							'type'    => 'boolean',
							'default' => false,
						),
					),
				),
			)
		);

		register_rest_route(
			self::REST_NAMESPACE,
			'/state',
			array(
				array(
					'methods'             => \WP_REST_Server::CREATABLE,
					'callback'            => array( $this, 'update_state' ),
					'permission_callback' => array( $this, 'permission_check' ),
					'args'                => array(
						'key'   => array(
							'type'     => 'string',
							'required' => true,
							'pattern'  => '^[a-z_]+$',
						),
						'value' => array(
							'required' => true,
						),
					),
				),
			)
		);
	}

	/**
	 * Returns the current settings as a portable document.
	 *
	 * @return \WP_REST_Response
	 */
	public function export() {
		$settings = $this->plugin()->settings()->all();

		return rest_ensure_response(
			array(
				'_type'      => self::FILE_TYPE,
				'_version'   => MENUVERSE_VERSION,
				'_generated' => gmdate( 'c' ),
				'_site'      => wp_parse_url( home_url(), PHP_URL_HOST ),
				'settings'   => array(
					'global'  => $settings['global'],
					'modules' => $settings['modules'],
				),
			)
		);
	}

	/**
	 * Applies an exported document, or reports what it would change.
	 *
	 * Menus themselves are never touched: a settings file references menus by
	 * id, and those ids mean nothing on another site, so a module whose menu is
	 * missing is reported rather than silently pointed somewhere wrong.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function import( \WP_REST_Request $request ) {
		$payload = (array) $request->get_param( 'payload' );
		$dry_run = (bool) $request->get_param( 'dry_run' );

		if ( ! isset( $payload['_type'] ) || self::FILE_TYPE !== $payload['_type'] ) {
			return new \WP_Error(
				'menuverse_bad_import',
				__( 'That file is not a MenuVerse settings export.', 'menuverse' ),
				array( 'status' => 400 )
			);
		}

		if ( empty( $payload['settings'] ) || ! is_array( $payload['settings'] ) ) {
			return new \WP_Error(
				'menuverse_bad_import',
				__( 'That export contains no settings.', 'menuverse' ),
				array( 'status' => 400 )
			);
		}

		$settings = $this->plugin()->settings();
		$registry = $this->plugin()->modules();
		$incoming = $payload['settings'];
		$report   = array(
			'applied' => array(),
			'skipped' => array(),
			'notes'   => array(),
		);

		if ( isset( $payload['_version'] ) && MENUVERSE_VERSION !== $payload['_version'] ) {
			$report['notes'][] = sprintf(
				/* translators: 1: version the file was written by, 2: version running now. */
				__( 'Written by version %1$s; you are running %2$s. Unknown settings are dropped.', 'menuverse' ),
				(string) $payload['_version'],
				MENUVERSE_VERSION
			);
		}

		$available = $this->available_menu_keys();

		foreach ( (array) ( isset( $incoming['modules'] ) ? $incoming['modules'] : array() ) as $module_id => $values ) {
			$module_id = (string) $module_id;

			if ( ! $registry->has( $module_id ) || ! is_array( $values ) ) {
				$report['skipped'][] = $module_id;
				continue;
			}

			if ( ! empty( $values['menu'] ) && ! in_array( $values['menu'], $available, true ) ) {
				$report['notes'][] = sprintf(
					/* translators: %s: module name. */
					__( '%s referenced a menu that does not exist here, so its menu was left unset.', 'menuverse' ),
					$module_id
				);

				$values['menu'] = '';
			}

			if ( $dry_run ) {
				$check = $settings->sanitize_module( $module_id, $values );

				if ( is_wp_error( $check ) ) {
					return $check;
				}
			} else {
				$result = $settings->update_module( $module_id, $values );

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

			$report['applied'][] = $module_id;
		}

		if ( isset( $incoming['global'] ) && is_array( $incoming['global'] ) && ! $dry_run ) {
			$result = $settings->update_global( $incoming['global'] );

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

		$report['dry_run']  = $dry_run;
		$report['settings'] = $dry_run ? null : $settings->all();

		/**
		 * Fires after a settings import has been applied.
		 *
		 * @since 1.0.0
		 *
		 * @param array $report  What was applied, skipped and noted.
		 * @param array $payload The imported document.
		 */
		if ( ! $dry_run ) {
			do_action( 'menuverse_settings_imported', $report, $payload );
		}

		return rest_ensure_response( $report );
	}

	/**
	 * Stores one dashboard state value.
	 *
	 * @param \WP_REST_Request $request Request object.
	 * @return \WP_REST_Response
	 */
	public function update_state( \WP_REST_Request $request ) {
		$key   = (string) $request->get_param( 'key' );
		$value = $request->get_param( 'value' );

		if ( is_array( $value ) || is_object( $value ) ) {
			$value = '';
		} elseif ( is_string( $value ) ) {
			$value = sanitize_text_field( $value );
		}

		$this->plugin()->settings()->set_state( $key, $value );

		return rest_ensure_response(
			array(
				'key'   => $key,
				'value' => $value,
			)
		);
	}

	/**
	 * Returns the menu keys this site can offer.
	 *
	 * @return string[]
	 */
	private function available_menu_keys() {
		$keys = array();

		foreach ( $this->plugin()->menu_sources()->list_menus() as $menu ) {
			$keys[] = $menu['source'] . ':' . $menu['id'];
		}

		return $keys;
	}
}
