<?php
/**
 * Settings storage shared by every module.
 *
 * @package MenuVerse
 */

namespace WPXero\MenuVerse\Settings;

use WPXero\MenuVerse\Modules\Module_Registry;

defined( 'ABSPATH' ) || exit;

/**
 * Reads and writes the single autoloaded options row backing the plugin.
 */
final class Settings {

	/**
	 * Option name holding user facing settings.
	 *
	 * @var string
	 */
	const OPTION = 'menuverse_settings';

	/**
	 * Option name holding internal, non frontend state.
	 *
	 * @var string
	 */
	const STATE_OPTION = 'menuverse_state';

	/**
	 * Module registry used to resolve per module schemas.
	 *
	 * @var Module_Registry|null
	 */
	private $registry = null;

	/**
	 * Request level cache of the merged settings tree.
	 *
	 * @var array|null
	 */
	private $cache = null;

	/**
	 * Injects the module registry.
	 *
	 * @param Module_Registry $registry Module registry.
	 * @return void
	 */
	public function set_registry( Module_Registry $registry ) {
		$this->registry = $registry;
	}

	/**
	 * Returns the schema describing plugin wide settings.
	 *
	 * @return array
	 */
	public function global_schema() {
		$schema = array(
			'type'       => 'object',
			'properties' => array(
				'accent_color'             => array(
					'type'    => 'string',
					'format'  => 'color',
					'default' => '#2563eb',
					'ui'      => array(
						'control' => 'color',
						'group'   => 'appearance',
						'label'   => __( 'Highlight colour', 'menuverse' ),
						'help'    => __( 'Used across every menu for the current page and for anything that needs to stand out.', 'menuverse' ),
						'token'   => '--menuverse-accent',
					),
				),
				'radius'                   => array(
					'type'    => 'string',
					'format'  => 'css-size',
					'default' => '8px',
					'ui'      => array(
						'control' => 'unit',
						'group'   => 'appearance',
						'label'   => __( 'How rounded corners are', 'menuverse' ),
						'help'    => __( 'Zero gives you square corners.', 'menuverse' ),
						'token'   => '--menuverse-radius',
					),
				),
				'delete_data_on_uninstall' => array(
					'type'    => 'boolean',
					'default' => false,
					'ui'      => array(
						'control' => 'toggle',
						'group'   => 'advanced',
						'label'   => __( 'Erase everything if this plugin is deleted', 'menuverse' ),
						'help'    => __( 'Off by default so that deleting the plugin by accident does not take your settings with it. Your menus themselves are never touched either way.', 'menuverse' ),
					),
				),
				'z_index'                  => array(
					'type'    => 'integer',
					'default' => 99999,
					'minimum' => 1,
					'maximum' => 2147483647,
					'ui'      => array(
						'control'  => 'number',
						'group'    => 'advanced',
						'advanced' => true,
						'label'    => __( 'Stacking order', 'menuverse' ),
						'help'     => __( 'Only worth changing if something else on your site covers up a MenuVerse menu. A higher number sits in front.', 'menuverse' ),
						'token'    => '--menuverse-z',
					),
				),
			),
			'ui'         => array(
				'groups' => array(
					'appearance' => array(
						'label'       => __( 'Shared look', 'menuverse' ),
						'description' => __( 'Applies to every menu at once, so you set it here rather than five times over.', 'menuverse' ),
						'order'       => 10,
					),
					'advanced'   => array(
						'label' => __( 'Housekeeping', 'menuverse' ),
						'order' => 20,
					),
				),
			),
		);

		/**
		 * Filters the plugin wide settings schema.
		 *
		 * @since 1.0.0
		 *
		 * @param array $schema Schema definition.
		 */
		return apply_filters( 'menuverse_global_schema', $schema );
	}

	/**
	 * Returns the full settings tree merged over defaults.
	 *
	 * @return array
	 */
	public function all() {
		if ( null !== $this->cache ) {
			return $this->cache;
		}

		$stored = get_option( self::OPTION, array() );
		$stored = is_array( $stored ) ? $stored : array();

		$settings = array(
			'_version' => isset( $stored['_version'] ) ? (string) $stored['_version'] : MENUVERSE_VERSION,
			'global'   => Schema_Sanitizer::sanitize(
				isset( $stored['global'] ) ? $stored['global'] : array(),
				$this->global_schema()
			),
			'modules'  => array(),
		);

		foreach ( $this->module_schemas() as $module_id => $schema ) {
			$settings['modules'][ $module_id ] = Schema_Sanitizer::sanitize(
				isset( $stored['modules'][ $module_id ] ) ? $stored['modules'][ $module_id ] : array(),
				$schema
			);
		}

		/**
		 * Filters the resolved settings tree.
		 *
		 * @since 1.0.0
		 *
		 * @param array $settings Settings tree.
		 */
		$this->cache = apply_filters( 'menuverse_settings', $settings );

		return $this->cache;
	}

	/**
	 * Returns settings for a single module.
	 *
	 * @param string $module_id Module identifier.
	 * @return array
	 */
	public function get_module( $module_id ) {
		$settings = $this->all();

		return isset( $settings['modules'][ $module_id ] ) ? $settings['modules'][ $module_id ] : array();
	}

	/**
	 * Returns a plugin wide setting.
	 *
	 * @param string $key           Setting key.
	 * @param mixed  $default_value Value returned when the key is unknown.
	 * @return mixed
	 */
	public function get_global( $key, $default_value = null ) {
		$settings = $this->all();

		return array_key_exists( $key, $settings['global'] ) ? $settings['global'][ $key ] : $default_value;
	}

	/**
	 * Validates and stores settings for a single module.
	 *
	 * @param string $module_id Module identifier.
	 * @param array  $data      Partial settings to merge.
	 * @return array|\WP_Error Stored module settings on success.
	 */
	public function update_module( $module_id, array $data ) {
		$schemas = $this->module_schemas();

		if ( ! isset( $schemas[ $module_id ] ) ) {
			return new \WP_Error(
				'menuverse_unknown_module',
				__( 'Unknown module.', 'menuverse' ),
				array( 'status' => 404 )
			);
		}

		$schema  = $schemas[ $module_id ];
		$merged  = array_merge( $this->get_module( $module_id ), $data );
		$cleaned = Schema_Sanitizer::sanitize( $merged, $schema );
		$valid   = Schema_Validator::validate( $cleaned, $schema, $module_id );

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

		/**
		 * Filters module settings immediately before they are written.
		 *
		 * @since 1.0.0
		 *
		 * @param array  $cleaned   Sanitized settings.
		 * @param string $module_id Module identifier.
		 * @param array  $data      Raw partial payload.
		 */
		$cleaned = apply_filters( "menuverse_{$module_id}_pre_save_settings", $cleaned, $module_id, $data );

		$stored                          = $this->raw();
		$stored['modules'][ $module_id ] = $cleaned;
		$stored['_version']              = MENUVERSE_VERSION;

		$this->write( $stored );

		/**
		 * Fires after a module's settings have been saved.
		 *
		 * @since 1.0.0
		 *
		 * @param array  $cleaned   Stored settings.
		 * @param string $module_id Module identifier.
		 */
		do_action( 'menuverse_module_settings_saved', $cleaned, $module_id );

		return $cleaned;
	}

	/**
	 * Sanitizes and validates module settings without persisting them.
	 *
	 * Used by the preview endpoint so unsaved values follow exactly the same
	 * rules as saved ones.
	 *
	 * @param string $module_id Module identifier.
	 * @param array  $data      Partial settings to merge over stored values.
	 * @return array|\WP_Error
	 */
	public function sanitize_module( $module_id, array $data ) {
		$schemas = $this->module_schemas();

		if ( ! isset( $schemas[ $module_id ] ) ) {
			return new \WP_Error(
				'menuverse_unknown_module',
				__( 'Unknown module.', 'menuverse' ),
				array( 'status' => 404 )
			);
		}

		$schema  = $schemas[ $module_id ];
		$merged  = array_merge( $this->get_module( $module_id ), $data );
		$cleaned = Schema_Sanitizer::sanitize( $merged, $schema );
		$valid   = Schema_Validator::validate( $cleaned, $schema, $module_id );

		return is_wp_error( $valid ) ? $valid : $cleaned;
	}

	/**
	 * Validates and stores plugin wide settings.
	 *
	 * @param array $data Partial settings to merge.
	 * @return array|\WP_Error
	 */
	public function update_global( array $data ) {
		$schema  = $this->global_schema();
		$merged  = array_merge( $this->all()['global'], $data );
		$cleaned = Schema_Sanitizer::sanitize( $merged, $schema );
		$valid   = Schema_Validator::validate( $cleaned, $schema, 'global' );

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

		$stored             = $this->raw();
		$stored['global']   = $cleaned;
		$stored['_version'] = MENUVERSE_VERSION;

		$this->write( $stored );

		return $cleaned;
	}

	/**
	 * Enables or disables a module.
	 *
	 * @param string $module_id Module identifier.
	 * @param bool   $enabled   Desired state.
	 * @return array|\WP_Error Stored module settings.
	 */
	public function set_module_enabled( $module_id, $enabled ) {
		return $this->update_module( $module_id, array( 'enabled' => (bool) $enabled ) );
	}

	/**
	 * Returns a value from the internal state option.
	 *
	 * @param string $key           State key.
	 * @param mixed  $default_value Value returned when the key is unknown.
	 * @return mixed
	 */
	public function get_state( $key, $default_value = null ) {
		$state = get_option( self::STATE_OPTION, array() );

		return is_array( $state ) && array_key_exists( $key, $state ) ? $state[ $key ] : $default_value;
	}

	/**
	 * Writes a value to the internal state option.
	 *
	 * @param string $key   State key.
	 * @param mixed  $value Value to store.
	 * @return void
	 */
	public function set_state( $key, $value ) {
		$state = get_option( self::STATE_OPTION, array() );
		$state = is_array( $state ) ? $state : array();

		$state[ $key ] = $value;

		update_option( self::STATE_OPTION, $state, false );
	}

	/**
	 * Returns the default settings tree.
	 *
	 * @return array
	 */
	public function defaults() {
		$defaults = array(
			'_version' => MENUVERSE_VERSION,
			'global'   => Schema::defaults( $this->global_schema() ),
			'modules'  => array(),
		);

		foreach ( $this->module_schemas() as $module_id => $schema ) {
			$defaults['modules'][ $module_id ] = Schema::defaults( $schema );
		}

		return $defaults;
	}

	/**
	 * Clears the request level cache.
	 *
	 * @return void
	 */
	public function flush() {
		$this->cache = null;
	}

	/**
	 * Returns every registered module schema keyed by module id.
	 *
	 * @return array
	 */
	private function module_schemas() {
		return $this->registry instanceof Module_Registry ? $this->registry->schemas() : array();
	}

	/**
	 * Reads the raw stored option without merging defaults.
	 *
	 * @return array
	 */
	private function raw() {
		$stored = get_option( self::OPTION, array() );
		$stored = is_array( $stored ) ? $stored : array();

		if ( ! isset( $stored['modules'] ) || ! is_array( $stored['modules'] ) ) {
			$stored['modules'] = array();
		}

		if ( ! isset( $stored['global'] ) || ! is_array( $stored['global'] ) ) {
			$stored['global'] = array();
		}

		return $stored;
	}

	/**
	 * Persists the option and clears the cache.
	 *
	 * @param array $stored Full option value.
	 * @return void
	 */
	private function write( array $stored ) {
		update_option( self::OPTION, $stored, true );
		$this->flush();
	}
}
