<?php
/**
 * File Path: includes/import/class-mbs-import-settings-applier.php
 *
 * MBS Import Settings Applier
 *
 * Authoritative write path for settings backup/restore.
 * Called by MBS_Tab_Child_Settings_Import::handle_ajax_mbs_import_settings_apply()
 * (the apply AJAX handler).
 *
 * Extracted from MBS_Import_Orchestrator per the SRP rule in Sprint Plan
 * (IEF-A-012): when settings application grows beyond ~50 lines it must live
 * in a dedicated helper class.
 *
 * Security contract:
 *   - Only keys matching the 'mbs_setting_' prefix are written. No other
 *     wp_options keys are touched, regardless of what is in the import data.
 *   - Keys in MBS_Settings_IO_Keys::EXCLUDED_KEYS are never written (they are
 *     stripped on export; this class provides a second line of defense).
 *   - Values are sanitized per type before storage (S-01):
 *     CSS values: wp_strip_all_tags() (C-003 — preserves CSS rules).
 *     Strings:    sanitize_text_field().
 *     Numerics:   intval() / floatval().
 *     Arrays:     array_map( 'sanitize_text_field', ... ) (shallow pass).
 *   - Section order is routed to MBS_Section_Order_DAO instead of wp_options
 *     (PLG-023 / S-02 fix: the wp_options key is ephemeral).
 *
 * @package    My_Book_Showroom
 * @since      1.0.0
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class MBS_Import_Settings_Applier { // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedClassFound -- mbs_/MBS_ prefix is correct; scanner not configured with plugin prefix

	/**
	 * Apply settings data to the current site.
	 *
	 * Handles the full write pipeline: key-prefix filter, excluded-key guard,
	 * per-type sanitization, section-order DAO routing, and wp_options write
	 * with autoload=false.
	 *
	 * @param array $settings Decoded settings array (from a settings JSON export).
	 * @return array{applied: int, skipped: int, errors: string[]} Result summary.
	 */
	public static function apply( array $settings ): array {
		$result = array(
			'applied' => 0,
			'skipped' => 0,
			'errors'  => array(),
		);

		$excluded = MBS_Settings_IO_Keys::get_excluded_keys();

		foreach ( $settings as $key => $value ) {
			// Skip the export metadata block and the _section_order custom block.
			if ( '_meta' === $key ) {
				$result['skipped']++;
				continue;
			}

			// S-02: Global default section order travels as a custom _section_order
			// block (not a wp_options key) and must be routed to the DAO.
			if ( '_section_order' === $key ) {
				self::apply_section_order( $value );
				$result['applied']++;
				continue;
			}

			// Only write mbs_setting_* keys.
			if ( 0 !== strpos( $key, 'mbs_setting_' ) ) {
				$result['skipped']++;
				continue;
			}

			// Second line of defense: never write excluded/sensitive keys.
			if ( in_array( $key, $excluded, true ) ) {
				$result['skipped']++;
				continue;
			}

			$value = self::sanitize_value( $key, $value );

			// Write with autoload=false for all imported settings.
			$updated = update_option( $key, $value, false );

			if ( false === $updated && get_option( $key ) !== $value ) {
				$result['errors'][] = 'Failed to write option: ' . esc_html( $key );
			} else {
				$result['applied']++;
			}
		}

		// Flush the alloptions cache so subsequent get_option() calls see the new values.
		delete_transient( 'mbs_settings_cache' );

		return $result;
	}

	/**
	 * Sanitize a settings value before storage.
	 *
	 * C-003: mbs_setting_appearance_custom_css stores raw CSS — sanitize_text_field()
	 * strips valid rule declarations (colons, semicolons). Use wp_strip_all_tags()
	 * to block HTML/script injection while preserving CSS syntax.
	 *
	 * @param string $key   Option key.
	 * @param mixed  $value Raw value from the import JSON.
	 * @return mixed Sanitized value.
	 */
	private static function sanitize_value( string $key, mixed $value ): mixed {
		if ( 'mbs_setting_appearance_custom_css' === $key ) {
			return is_string( $value ) ? wp_strip_all_tags( $value ) : '';
		}

		if ( is_string( $value ) ) {
			return sanitize_text_field( $value );
		}

		if ( is_numeric( $value ) ) {
			return is_float( $value ) ? floatval( $value ) : intval( $value );
		}

		if ( is_array( $value ) ) {
			return array_map( 'sanitize_text_field', $value );
		}

		// V7-D-006: Explicit bool and null handling to match WordPress convention.
		if ( is_bool( $value ) ) {
			return $value ? '1' : '0';
		}
		if ( is_null( $value ) ) {
			return '';
		}

		return $value;
	}

	/**
	 * Apply the exported global default section order via the DAO.
	 *
	 * S-02 / PLG-023: Section order is stored in the mbs_section_order table
	 * (post_id = 0 for global defaults), not in wp_options. The export embeds
	 * it as _section_order => { order: [...], visibility: {...} }. This method
	 * routes those values to the correct DAO calls.
	 *
	 * @param mixed $data Decoded _section_order block from the export JSON.
	 * @return void
	 */
	private static function apply_section_order( mixed $data ): void {
		if ( ! is_array( $data ) ) {
			return;
		}

		$sections = isset( $data['order'] ) && is_array( $data['order'] )
			? array_values( array_filter( array_map( 'sanitize_key', $data['order'] ) ) )
			: array();

		if ( ! empty( $sections ) ) {
			MBS_Section_Order_DAO::save_order( 0, $sections );
		}

		if ( isset( $data['visibility'] ) && is_array( $data['visibility'] ) ) {
			$visibility_map = array();
			foreach ( $data['visibility'] as $section_key => $visible ) {
				$safe_key = sanitize_key( (string) $section_key );
				if ( $safe_key ) {
					$visibility_map[ $safe_key ] = ! empty( $visible );
				}
			}
			if ( ! empty( $visibility_map ) ) {
				MBS_Section_Order_DAO::save_global_visibility( $visibility_map );
			}
		}
	}
}
