<?php
/**
 * Owns the single root option `bw_dev_settings`. All persisted state for every
 * module lives nested under this one key. Sanitization on save dispatches to
 * each module's sanitize() method based on the active tab.
 *
 * @package BW_Dev
 */

defined( 'ABSPATH' ) || exit;

class BW_Dev_Settings {

	const OPTION       = 'bw_dev_settings';
	const SETTING_GROUP = 'bw_dev';

	/**
	 * Cached option payload. Populated lazily; invalidated on update().
	 *
	 * @var array|null
	 */
	private $cache = null;

	/**
	 * Module registry (slug => BW_Dev_Module_Interface). Injected by
	 * BW_Dev_Plugin after both have been constructed so the dispatching
	 * sanitize callback can route per-module data correctly.
	 *
	 * @var BW_Dev_Module_Interface[]
	 */
	private $modules = array();

	public function set_modules( array $modules ): void {
		$this->modules = $modules;
	}

	/**
	 * Hook into WordPress. Called from BW_Dev_Plugin::boot() on plugins_loaded.
	 */
	public function register(): void {
		add_action( 'admin_init', array( $this, 'register_setting' ) );
	}

	public function register_setting(): void {
		register_setting(
			self::SETTING_GROUP,
			self::OPTION,
			array(
				'type'              => 'array',
				'sanitize_callback' => array( $this, 'sanitize' ),
				'default'           => array(),
			)
		);
	}

	/**
	 * Full option payload, with cache.
	 */
	public function all(): array {
		if ( null === $this->cache ) {
			$raw         = get_option( self::OPTION, array() );
			$this->cache = is_array( $raw ) ? $raw : array();
		}
		return $this->cache;
	}

	/**
	 * Typed accessor. Returns $default if the section or key is absent.
	 *
	 * @param string      $section Module slug or top-level section (`modules`, `brand`).
	 * @param string|null $key     Nested key inside the section. null = whole section.
	 * @param mixed       $default Fallback when the key is absent.
	 */
	public function get( string $section, ?string $key = null, $default = null ) {
		$all = $this->all();
		if ( ! isset( $all[ $section ] ) ) {
			return null === $key ? $default : $default;
		}
		if ( null === $key ) {
			return $all[ $section ];
		}
		if ( is_array( $all[ $section ] ) && array_key_exists( $key, $all[ $section ] ) ) {
			return $all[ $section ][ $key ];
		}
		return $default;
	}

	/**
	 * Replace a single section of the option. Use sparingly — most writes
	 * should flow through the sanitize callback so dispatching stays
	 * centralized. This setter is for code paths outside the Settings API
	 * (activation hook, migration, programmatic toggles).
	 */
	public function update_section( string $section, $value ): void {
		$all             = $this->all();
		$all[ $section ] = $value;
		update_option( self::OPTION, $all );
		$this->cache = $all;
	}

	/**
	 * Default enable-state per sidebar group. Used when a module's slug isn't
	 * yet in the saved modules map (fresh install, or a module added in a
	 * later plugin version that the user hasn't re-saved over).
	 *
	 * Block modules default OFF — they're opt-in, since each adds an editor
	 * inserter entry the site might not want. Everything else defaults ON.
	 *
	 * @var array<string,bool>
	 */
	private const GROUP_DEFAULTS = array(
		'core'         => true,
		'editor_admin' => true,
		'frontend'     => true,
		'security'     => true,
		'indexing'     => true,
		'blocks'       => false,
		'vendors'      => true,
	);

	/**
	 * Whether a module is enabled. Two-step resolution:
	 *   1. If the slug is in the saved modules map, use that boolean (the
	 *      user has explicitly chosen).
	 *   2. Otherwise fall back to the per-group default (see GROUP_DEFAULTS),
	 *      so a module added in a later version lights up sensibly without
	 *      requiring a re-save.
	 */
	public function is_module_enabled( string $slug ): bool {
		$map = $this->get( 'modules' );
		if ( is_array( $map ) && array_key_exists( $slug, $map ) ) {
			$enabled = (bool) $map[ $slug ];
		} else {
			$enabled = $this->default_enabled_for_slug( $slug );
		}
		/**
		 * Override per-module enable. Useful for hosting environments that
		 * force-disable a module via mu-plugins.
		 *
		 * @param bool   $enabled Whether the module is on.
		 * @param string $slug    Module slug.
		 */
		return (bool) apply_filters( 'bw_dev_module_enabled', $enabled, $slug );
	}

	/**
	 * Per-group default for a slug not yet in the saved modules map. Looks up
	 * the module in the registry to read its group(), then maps to the
	 * `GROUP_DEFAULTS` table. Filterable via `bw_dev_module_default_enabled`
	 * so mu-plugins can override per-site.
	 */
	private function default_enabled_for_slug( string $slug ): bool {
		$group = '';
		foreach ( $this->modules as $module ) {
			if ( $module->slug() === $slug ) {
				$group = $module->group();
				break;
			}
		}
		$default = isset( self::GROUP_DEFAULTS[ $group ] ) ? self::GROUP_DEFAULTS[ $group ] : true;
		/**
		 * Override the per-group default for a module that has no saved state
		 * yet. Once the user saves the Modules tab once, that explicit choice
		 * wins and this filter is no longer consulted for that slug.
		 *
		 * @param bool   $default Default enable-state for this slug's group.
		 * @param string $slug    Module slug.
		 * @param string $group   Module's declared group ('core', 'editor_admin', 'frontend', 'security', 'indexing', 'blocks', or '' if not registered yet).
		 */
		return (bool) apply_filters( 'bw_dev_module_default_enabled', $default, $slug, $group );
	}

	/**
	 * Dispatching sanitize callback. The Settings API hands us the entire
	 * submitted payload for the option. We figure out which tab posted
	 * based on the hidden `__tab` field, run the appropriate sanitize, and
	 * merge into the previously-saved data so other tabs' values survive.
	 *
	 * @param mixed $raw Submitted data from $_POST.
	 * @return array Full option value to persist.
	 */
	public function sanitize( $raw ): array {
		$existing = $this->all();
		if ( ! is_array( $raw ) ) {
			return $existing;
		}
		$tab = isset( $raw['__tab'] ) ? sanitize_key( $raw['__tab'] ) : '';
		unset( $raw['__tab'] );

		switch ( $tab ) {
			case 'modules':
				$map = array();
				foreach ( $this->modules as $module ) {
					$slug         = $module->slug();
					$map[ $slug ] = ! empty( $raw['modules'][ $slug ] );
				}
				$existing['modules'] = $map;
				break;

			case 'branding':
				// Phase 7 fills this in. For now accept a known shape.
				$brand = isset( $raw['brand'] ) && is_array( $raw['brand'] ) ? $raw['brand'] : array();
				$existing['brand'] = array(
					'plugin_display_name'  => isset( $brand['plugin_display_name'] ) ? sanitize_text_field( $brand['plugin_display_name'] ) : '',
					'block_category_label' => isset( $brand['block_category_label'] ) ? sanitize_text_field( $brand['block_category_label'] ) : '',
					'block_title_prefix'   => isset( $brand['block_title_prefix'] ) ? sanitize_text_field( $brand['block_title_prefix'] ) : '',
				);
				break;

			default:
				foreach ( $this->modules as $module ) {
					if ( $module->slug() === $tab ) {
						$section_raw       = isset( $raw[ $tab ] ) && is_array( $raw[ $tab ] ) ? $raw[ $tab ] : array();
						$existing[ $tab ] = $module->sanitize( $section_raw );
						break;
					}
				}
				break;
		}

		$this->cache = $existing;
		return $existing;
	}
}
