<?php
/**
 * Validates widget settings against Elementor control schemas.
 *
 * @package EMCP_Tools
 * @since   1.0.0
 */

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

/**
 * Validates widget settings against their control definitions.
 *
 * @since 1.0.0
 */
class EMCP_Tools_Settings_Validator {

	/**
	 * The schema generator instance.
	 *
	 * @var EMCP_Tools_Schema_Generator
	 */
	private $schema_generator;

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param EMCP_Tools_Schema_Generator $schema_generator The schema generator.
	 */
	public function __construct( EMCP_Tools_Schema_Generator $schema_generator ) {
		$this->schema_generator = $schema_generator;
	}

	/**
	 * Responsive breakpoint suffixes generated by Elementor's add_responsive_control().
	 *
	 * @var string[]
	 */
	private static $responsive_suffixes = array(
		'_mobile',
		'_tablet',
		'_widescreen',
		'_laptop',
		'_mobile_extra',
		'_tablet_extra',
	);

	/**
	 * Common advanced control keys valid for ALL widget types.
	 * From Elementor's Widget_Common_Base (common-base.php).
	 *
	 * @var string[]
	 */
	private static $common_advanced_keys = array(
		// Positioning.
		'_element_width', '_element_custom_width', '_element_vertical_align',
		'_position', '_offset_orientation_h', '_offset_x', '_offset_x_end',
		'_offset_orientation_v', '_offset_y', '_offset_y_end',
		'_z_index', '_element_id', '_css_classes',
		// Spacing.
		'_margin', '_padding',
		// Background (normal + hover).
		'_background_background', '_background_color', '_background_image',
		'_background_position', '_background_attachment', '_background_repeat',
		'_background_size', '_background_bg_width',
		'_background_hover_background', '_background_hover_color',
		'_background_hover_transition',
		// Border (normal + hover).
		'_border_border', '_border_width', '_border_color',
		'_border_radius', '_border_hover_border', '_border_hover_width',
		'_border_hover_color', '_border_radius_hover',
		'_border_hover_transition',
		// Box shadow (normal + hover).
		'_box_shadow_box_shadow_type', '_box_shadow_box_shadow',
		'_box_shadow_hover_box_shadow_type', '_box_shadow_hover_box_shadow',
		// Mask.
		'_mask_switch', '_mask_shape', '_mask_image',
		'_mask_size', '_mask_size_scale',
		'_mask_position', '_mask_position_x', '_mask_position_y',
		'_mask_repeat',
		// Animation.
		'_animation', 'animation_duration', '_animation_delay',
		// Flex item.
		'_flex_align_self', '_flex_order', '_flex_order_custom',
		'_flex_size', '_flex_grow', '_flex_shrink',
		// Grid item.
		'_grid_column', '_grid_column_custom', '_grid_row', '_grid_row_custom',
		// Responsive visibility.
		'hide_desktop', 'hide_tablet', 'hide_mobile',
		// Motion effects (Pro) — actual Elementor schema keys use motion_fx_ prefix.
		'motion_fx_motion_fx_scrolling', 'motion_fx_motion_fx_mouse',
		'motion_fx_devices', 'motion_fx_range',
		'motion_fx_translateY_effect', 'motion_fx_translateY_direction',
		'motion_fx_translateY_speed', 'motion_fx_translateY_affectedRange',
		'motion_fx_translateX_effect', 'motion_fx_translateX_direction',
		'motion_fx_translateX_speed', 'motion_fx_translateX_affectedRange',
		'motion_fx_opacity_effect', 'motion_fx_opacity_direction',
		'motion_fx_opacity_level', 'motion_fx_opacity_range',
		'motion_fx_blur_effect', 'motion_fx_blur_direction',
		'motion_fx_blur_level', 'motion_fx_blur_range',
		'motion_fx_rotateZ_effect', 'motion_fx_rotateZ_direction',
		'motion_fx_rotateZ_speed', 'motion_fx_rotateZ_affectedRange',
		'motion_fx_scale_effect', 'motion_fx_scale_direction',
		'motion_fx_scale_speed', 'motion_fx_scale_range',
		'motion_fx_tilt_effect', 'motion_fx_tilt_direction', 'motion_fx_tilt_speed',
		'motion_fx_mouseTrack_effect', 'motion_fx_mouseTrack_direction', 'motion_fx_mouseTrack_speed',
		'motion_fx_transform_origin_x', 'motion_fx_transform_origin_y',
		'motion_fx_transform_x_anchor_point', 'motion_fx_transform_y_anchor_point',
		'handle_motion_fx_asset_loading',
		// CSS Transform controls.
		'_transform_rotate_popover', '_transform_rotateZ_effect',
		'_transform_rotate_3d', '_transform_rotateX_effect', '_transform_rotateY_effect',
		'_transform_translate_popover', '_transform_translateX_effect', '_transform_translateY_effect',
		'_transform_scale_popover', '_transform_scale_effect',
		'_transform_scaleX_effect', '_transform_scaleY_effect',
		'_transform_rotate_popover_hover', '_transform_rotateZ_effect_hover',
		'_transform_rotate_3d_hover', '_transform_rotateX_effect_hover', '_transform_rotateY_effect_hover',
		'_transform_translate_popover_hover', '_transform_translateX_effect_hover', '_transform_translateY_effect_hover',
		'_transform_scale_popover_hover', '_transform_scale_effect_hover',
		'_transform_scaleX_effect_hover', '_transform_scaleY_effect_hover',
	);

	/**
	 * Validates settings for a widget type (non-fatal / advisory).
	 *
	 * IMPORTANT: this never hard-rejects a setting. Elementor's headless
	 * `get_widget_types($t)->get_controls()` — the source our schema is built
	 * from — systematically OMITS group controls (typography) and tabbed style
	 * controls (e.g. `title_color`) for every widget in a REST/CLI context, even
	 * though they are real controls registered in the widget's `register_controls()`
	 * (confirmed in Elementor core, e.g. heading.php's `title_color` + `typography`).
	 * Treating that incomplete list as an allow-list previously rejected valid
	 * styling and aborted the whole insert.
	 *
	 * Elementor itself is the real authority: it applies the controls it knows on
	 * render and ignores genuinely-unknown keys. So we pass settings through and
	 * only LOG keys we can't account for (diagnostics), never block.
	 *
	 * @since 1.0.0
	 * @since 2.2.0 Non-fatal: returns true for unrecognised keys (logs them under WP_DEBUG)
	 *              instead of returning a WP_Error.
	 *
	 * @param string $widget_type The widget type name.
	 * @param array  $settings    The settings to validate.
	 * @return true Always true; the method is advisory only.
	 */
	public function validate( string $widget_type, array $settings ) {
		$schema = $this->schema_generator->generate( $widget_type );

		// Can't introspect controls (unknown widget, Elementor unavailable, etc.):
		// don't block — Elementor is the authority at save/render time.
		if ( is_wp_error( $schema ) ) {
			return true;
		}

		$valid_keys = array_keys( $schema['properties'] ?? array() );
		$unknown    = array();

		foreach ( array_keys( $settings ) as $key ) {
			if ( in_array( $key, $valid_keys, true )
				|| in_array( $key, self::$common_advanced_keys, true )
				|| $this->is_responsive_variant( $key, $valid_keys )
				|| $this->is_group_control_subkey( $key, $valid_keys )
			) {
				continue;
			}
			$unknown[] = $key;
		}

		// Unknown keys are very often real controls the headless schema can't see
		// (typography, colours). Pass them through; log for diagnostics only.
		if ( ! empty( $unknown ) && defined( 'WP_DEBUG' ) && WP_DEBUG ) {
			error_log(
				sprintf(
					'[EMCP Tools] Passing through %1$d unrecognised setting(s) for widget "%2$s" (absent from the headless control schema): %3$s',
					count( $unknown ),
					$widget_type,
					implode( ', ', $unknown )
				)
			);
		}

		return true;
	}

	/**
	 * Checks if a key is a responsive variant of a valid schema key or common key.
	 *
	 * @param string   $key        The settings key to check.
	 * @param string[] $valid_keys Valid keys from the widget schema.
	 * @return bool
	 */
	/**
	 * Checks if a key is a sub-field of a group control that has other keys in the schema.
	 *
	 * Group controls in Elementor use a shared prefix (e.g. 'text_stroke_', 'typography_',
	 * 'border_'). If any key with the same prefix exists in the schema, other sub-fields
	 * with that prefix are also valid.
	 *
	 * @param string   $key        The settings key to check.
	 * @param string[] $valid_keys Valid keys from the widget schema.
	 * @return bool
	 */
	private function is_group_control_subkey( string $key, array $valid_keys ): bool {
		// Try progressively shorter prefixes (split on '_').
		$parts = explode( '_', $key );
		// Need at least 2 parts for a group prefix (e.g. 'text_stroke' from 'text_stroke_stroke_width').
		for ( $i = 1, $len = count( $parts ) - 1; $i <= $len; $i++ ) {
			$prefix = implode( '_', array_slice( $parts, 0, $i ) ) . '_';
			foreach ( $valid_keys as $valid_key ) {
				if ( str_starts_with( $valid_key, $prefix ) && $valid_key !== $key ) {
					return true;
				}
			}
		}
		return false;
	}

	private function is_responsive_variant( string $key, array $valid_keys ): bool {
		foreach ( self::$responsive_suffixes as $suffix ) {
			if ( str_ends_with( $key, $suffix ) ) {
				$base_key = substr( $key, 0, -strlen( $suffix ) );
				if ( in_array( $base_key, $valid_keys, true ) ) {
					return true;
				}
				if ( in_array( $base_key, self::$common_advanced_keys, true ) ) {
					return true;
				}
			}
		}

		return false;
	}
}
