<?php
/**
 * POST Data Parser for v4.4.0+
 *
 * Handles JSON-encoded field data for optimal performance.
 * Each field sends 1 JSON string instead of 259+ inputs.
 *
 * @package Extra_Product_Options_For_WooCommerce
 * @since   4.4.0
 */

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

/**
 * EPOFW POST Parser
 */
class EPOFW_Post_Parser {

	/**
	 * Parse submitted addon data
	 *
	 * @since 4.4.0
	 * @param array $post_data Raw $_POST data.
	 * @return array Normalized addon data structure.
	 */
	public static function parse_addon_data( $post_data ) {
		// Expand JSON-encoded field data (1 JSON input per field).
		if ( ! empty( $post_data['epofw_field_json'] ) && is_array( $post_data['epofw_field_json'] ) ) {
			$post_data = self::expand_json_fields( $post_data );
		}

		// Parse flat structure.
		$has_flat_sections   = isset( $post_data['epofw_section'] );
		$has_flat_row_groups = isset( $post_data['epofw_row_group'] );
		$has_flat_columns    = isset( $post_data['epofw_column'] );
		$has_flat_fields     = isset( $post_data['epofw_field'] );

		if ( $has_flat_sections || $has_flat_row_groups || $has_flat_columns || $has_flat_fields ) {
			return self::parse_flat_structure( $post_data );
		}

		return array();
	}

	/**
	 * Expand JSON-encoded field data into epofw_field format.
	 *
	 * JavaScript sends: epofw_field_json[field_id] = JSON string with all field data
	 * This converts it to: epofw_field[field_id] = array with field data
	 *
	 * @since 4.4.0
	 * @param array $post_data POST data containing epofw_field_json.
	 * @return array POST data with epofw_field populated from JSON.
	 */
	private static function expand_json_fields( $post_data ) {
		$post_data['epofw_field'] = array();

		foreach ( $post_data['epofw_field_json'] as $field_id => $json_string ) {
			// Don't use stripslashes() - it breaks backslashes in file paths.
			// JSON.stringify() in JavaScript already properly escapes everything.
			$field_data = json_decode( $json_string, true );

			if ( ! is_array( $field_data ) ) {
				continue;
			}

			// Map JSON structure to epofw_field format.
			$post_data['epofw_field'][ $field_id ] = array(
				'section_id'    => isset( $field_data['section_id'] ) ? $field_data['section_id'] : '',
				'row_id'        => isset( $field_data['row_id'] ) ? $field_data['row_id'] : '',
				'group_id'      => isset( $field_data['group_id'] ) ? $field_data['group_id'] : '',
				'columns_id'    => isset( $field_data['columns_id'] ) ? $field_data['columns_id'] : '',
				'columns_order' => isset( $field_data['columns_order'] ) ? $field_data['columns_order'] : 0,
				'layout'        => isset( $field_data['layout'] ) ? $field_data['layout'] : 'single',
				'columns'       => isset( $field_data['columns'] ) ? $field_data['columns'] : 1,
				'order'         => isset( $field_data['order'] ) ? $field_data['order'] : 0,
				'field_status'  => isset( $field_data['field_status'] ) && 'on' === $field_data['field_status'] ? 'on' : '',
				'field'         => isset( $field_data['field'] ) ? $field_data['field'] : array(),
				'label'         => isset( $field_data['label'] ) ? $field_data['label'] : array(),
			);

			// Copy complex nested structures.
			$complex_keys = array(
				'epofw_field_settings',
				'input_container_settings',
				'field_container_settings',
				'separator_settings',
				'logical_operation',
				'apodp_logical_operation',
				'common',
				'repeater_settings',
			);

			foreach ( $complex_keys as $key ) {
				if ( isset( $field_data[ $key ] ) && is_array( $field_data[ $key ] ) ) {
					$post_data['epofw_field'][ $field_id ][ $key ] = $field_data[ $key ];
				}
			}
		}

		unset( $post_data['epofw_field_json'] );

		return $post_data;
	}

	/**
	 * Parse flat structure
	 *
	 * @since 4.4.0
	 * @param array $post_data POST data with flat structure.
	 * @return array Normalized addon data.
	 */
	private static function parse_flat_structure( $post_data ) {
		$addon_name     = isset( $post_data['epofw_addon_name'] ) ? sanitize_text_field( $post_data['epofw_addon_name'] ) : '';
		$addon_status   = isset( $post_data['epofw_addon_status'] ) ? sanitize_text_field( $post_data['epofw_addon_status'] ) : 'on';
		$addon_position = isset( $post_data['epofw_addon_position'] ) ? sanitize_text_field( $post_data['epofw_addon_position'] ) : 'before_add_to_cart';
		// Section layout setting (premium only - accordion/tabs/wizard).
		$epofw_section_layout = isset( $post_data['epofw_section_layout'] ) ? sanitize_text_field( $post_data['epofw_section_layout'] ) : 'default';

		$addon_data = array(
			'epofw_addon_name'     => $addon_name,
			'epofw_addon_status'   => $addon_status,
			'epofw_addon_position' => $addon_position,
			'epofw_section_layout' => $epofw_section_layout,
			'general'              => array(),
		);

		// Section layout sub-options (premium only - accordion/tabs/wizard settings).
		$epofw_layout_settings_keys = array(
			'section_layout_accordion_mode',
			'section_layout_accordion_default_open',
			'section_layout_accordion_icon_style',
			'section_layout_tabs_style',
			'section_layout_tabs_position',
			'section_layout_wizard_show_progress',
			'section_layout_wizard_validate_steps',
			'section_layout_wizard_show_step_numbers',
		);
		foreach ( $epofw_layout_settings_keys as $epofw_layout_setting_key ) {
			if ( isset( $post_data[ $epofw_layout_setting_key ] ) ) {
				$addon_data[ $epofw_layout_setting_key ] = sanitize_text_field( $post_data[ $epofw_layout_setting_key ] );
			}
		}

		// Parse sections.
		// First, load existing sections from database to preserve status of sections not in POST data.
		$existing_sections = array();
		$addon_post_id     = isset( $post_data['addon_id'] ) ? absint( $post_data['addon_id'] ) : 0;
		// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification handled at function entry point (admin save action).
		if ( empty( $addon_post_id ) && isset( $_POST['addon_id'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Nonce verification handled at function entry point (admin save action).
			$addon_post_id = absint( $_POST['addon_id'] );
		}
		if ( $addon_post_id > 0 ) {
			$existing_addon = EPOFW_Custom_Tables::load_addon( $addon_post_id );
			if ( ! empty( $existing_addon['general'] ) ) {
				foreach ( $existing_addon['general'] as $existing_section_id => $existing_section_data ) {
					$existing_sections[ $existing_section_id ] = $existing_section_data;
				}
			}
		}

		if ( ! empty( $post_data['epofw_section'] ) ) {
			foreach ( $post_data['epofw_section'] as $section_id => $section_data ) {
				$section_settings = array();
				if ( isset( $section_data['settings'] ) && is_array( $section_data['settings'] ) ) {
					$section_settings = $section_data['settings'];
				}

				// Handle section title - preserve ampersand characters.
				$section_title = '';
				if ( isset( $section_data['title'] ) ) {
					// Unslash POST data first (WordPress adds slashes automatically).
					$section_title = wp_unslash( $section_data['title'] );
					// Decode HTML entities (e.g., &amp; becomes &).
					$section_title = html_entity_decode( $section_title, ENT_QUOTES | ENT_HTML5, 'UTF-8' );
					// Strip tags and trim, but preserve & character.
					$section_title = wp_strip_all_tags( $section_title );
					$section_title = trim( $section_title );
					// Remove invalid UTF-8 characters.
					$section_title = wp_check_invalid_utf8( $section_title, true );
				}

				// Handle section status from POST data.
				// POST field name is 'status' (not 'section_status').
				// - If 'status' is set and not empty: use it (explicitly enabled).
				// - If 'status' is set but empty: means disabled (explicitly unchecked).
				// - If 'status' is not set at all: preserve from DB (section not modified).
				$section_status = 'on';
				if ( isset( $section_data['status'] ) ) {
					// Status field exists in POST - use it (even if empty, which means disabled).
					if ( ! empty( $section_data['status'] ) ) {
						$section_status = sanitize_text_field( $section_data['status'] );
					} else {
						// Empty string means explicitly disabled.
						$section_status = '';
					}
				} elseif ( isset( $existing_sections[ $section_id ]['section_status'] ) ) {
					// Status field not in POST at all - preserve from DB (section not modified).
					$section_status = $existing_sections[ $section_id ]['section_status'];
				}

				// Preserve section_id exactly as it comes from POST (supports both old format: timestamp, and new format: post_id_timestamp).
				// Use the [id] field value if present, otherwise use the array key (section_id).
				$final_section_id = isset( $section_data['id'] ) ? sanitize_text_field( $section_data['id'] ) : sanitize_text_field( $section_id );

				$addon_data['general'][ $section_id ] = array(
					'section_id'       => $final_section_id,
					'section_title'    => $section_title,
					'section_status'   => $section_status,
					'section_order'    => isset( $section_data['order'] ) ? absint( $section_data['order'] ) : 0,
					'section_settings' => $section_settings,
					'fields'           => array(),
				);
			}
		}

		// Note: Sections NOT in POST data are intentionally NOT preserved.
		// When a section is removed from the UI, it should be deleted from the database.
		// The save_addon() function in EPOFW_Custom_Tables handles deletion of sections
		// that exist in DB but are not in the new data.

		// Parse row groups.
		if ( ! empty( $post_data['epofw_row_group'] ) ) {
			foreach ( $post_data['epofw_row_group'] as $group_id => $group_data ) {
				$section_id = isset( $group_data['section_id'] ) ? $group_data['section_id'] : '';

				if ( $section_id && isset( $addon_data['general'][ $section_id ] ) ) {
					unset( $group_data['section_id'], $group_data['group_id'] );

					if ( ! isset( $addon_data['general'][ $section_id ]['row_groups'] ) ) {
						$addon_data['general'][ $section_id ]['row_groups'] = array();
					}

					// Filter out style settings, keep only layout properties (gap, alignment).
					$addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['row_group_settings'] = self::filter_row_group_settings( $group_data );
				}
			}
		}

		// Parse columns groups (v2.3: new structure).
		if ( ! empty( $post_data['epofw_columns_group'] ) ) {
			foreach ( $post_data['epofw_columns_group'] as $columns_id => $columns_group_data ) {
				$section_id = isset( $columns_group_data['section_id'] ) ? $columns_group_data['section_id'] : '';
				$group_id   = isset( $columns_group_data['group_id'] ) ? $columns_group_data['group_id'] : '';

				if ( $section_id && $group_id && isset( $addon_data['general'][ $section_id ]['row_groups'][ $group_id ] ) ) {
					// Extract columns_count first to determine correct layout_type.
					$columns_count = isset( $columns_group_data['columns_count'] ) ? absint( $columns_group_data['columns_count'] ) : 1;

					// Determine layout_type based on columns_count if not provided, or validate provided value.
					$provided_layout_type = isset( $columns_group_data['layout_type'] ) ? sanitize_text_field( $columns_group_data['layout_type'] ) : '';

					// Auto-determine layout_type from columns_count to ensure consistency.
					if ( 1 === (int) $columns_count ) {
						$layout_type = 'single';
					} elseif ( 2 === (int) $columns_count ) {
						$layout_type = 'two';
					} elseif ( 3 === (int) $columns_count ) {
						$layout_type = 'three';
					} elseif ( 4 === (int) $columns_count ) {
						$layout_type = 'four';
					} else {
						// Fallback to provided or default to 'single'.
						$layout_type = $provided_layout_type ? $provided_layout_type : 'single';
					}

					unset( $columns_group_data['section_id'], $columns_group_data['group_id'], $columns_group_data['columns_id'] );

					if ( ! isset( $addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['columns_groups'] ) ) {
						$addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['columns_groups'] = array();
					}

					// Ensure layout_type and columns_count are properly set.
					$columns_group_data['layout_type']   = $layout_type;
					$columns_group_data['columns_count'] = $columns_count;
					$addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['columns_groups'][ $columns_id ] = $columns_group_data;
				}
			}
		}

		// Parse columns (individual columns within columns groups) - v2.3: updated structure.
		if ( ! empty( $post_data['epofw_column'] ) ) {
			// First pass: Group columns by their columns_id to ensure proper structure.
			$columns_by_group = array();

			foreach ( $post_data['epofw_column'] as $row_id => $column_data ) {
				$section_id    = isset( $column_data['section_id'] ) ? $column_data['section_id'] : '';
				$group_id      = isset( $column_data['group_id'] ) ? $column_data['group_id'] : '';
				$columns_id    = isset( $column_data['columns_id'] ) && ! empty( $column_data['columns_id'] ) ? $column_data['columns_id'] : null;
				$columns_order = isset( $column_data['columns_order'] ) ? absint( $column_data['columns_order'] ) : 0;

				// For backward compatibility: use sub_row_id if columns_id is not provided.
				if ( ! $columns_id && isset( $column_data['sub_row_id'] ) && ! empty( $column_data['sub_row_id'] ) ) {
					$columns_id    = $column_data['sub_row_id'];
					$columns_order = isset( $column_data['sub_row_order'] ) ? absint( $column_data['sub_row_order'] ) : 0;
				}

				// If still no columns_id, generate one based on group_id (for rows that don't specify columns container).
				if ( ! $columns_id && $group_id ) {
					$columns_id    = 'columns_' . $group_id;
					$columns_order = 0;
				}

				if ( $section_id && $group_id && $columns_id ) {
					$key = $section_id . '_' . $group_id . '_' . $columns_id;
					if ( ! isset( $columns_by_group[ $key ] ) ) {
						$columns_by_group[ $key ] = array(
							'section_id'    => $section_id,
							'group_id'      => $group_id,
							'columns_id'    => $columns_id,
							'columns_order' => $columns_order,
							'columns'       => array(),
						);
					}
					$columns_by_group[ $key ]['columns'][ $row_id ] = $column_data;
				}
			}

			// Second pass: Process grouped columns.
			foreach ( $columns_by_group as $group_data ) {
				$section_id    = $group_data['section_id'];
				$group_id      = $group_data['group_id'];
				$columns_id    = $group_data['columns_id'];
				$columns_order = $group_data['columns_order'];

				// Ensure section structure exists.
				if ( ! isset( $addon_data['general'][ $section_id ]['row_groups'][ $group_id ] ) ) {
					$addon_data['general'][ $section_id ]['row_groups'][ $group_id ] = array(
						'row_group_settings' => array(),
						'columns_groups'     => array(),
					);
				}

				// Ensure columns_group exists.
				if ( ! isset( $addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['columns_groups'][ $columns_id ] ) ) {
					// Determine layout_type from columns count or default to 'single'.
					$layout_type   = 'single';
					$columns_count = count( $group_data['columns'] );
					if ( 2 === (int) $columns_count ) {
						$layout_type = 'two';
					} elseif ( 3 === (int) $columns_count ) {
						$layout_type = 'three';
					} elseif ( 4 === (int) $columns_count ) {
						$layout_type = 'four';
					}

					$addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['columns_groups'][ $columns_id ] = array(
						'columns_order' => $columns_order,
						'layout_type'   => $layout_type,
						'columns_count' => $columns_count,
						'rows'          => array(),
					);
				}

				// Process each column in this group.
				foreach ( $group_data['columns'] as $row_id => $column_data ) {
					// Remove metadata fields, keep only column data.
					unset( $column_data['section_id'], $column_data['group_id'], $column_data['row_id'], $column_data['columns_id'], $column_data['columns_order'], $column_data['sub_row_id'], $column_data['sub_row_order'] );

					// Filter column settings - columns are layout-only (no styling).
					$column_settings = isset( $column_data['column_settings'] ) ? $column_data['column_settings'] : array();
					$column_settings = self::filter_column_settings( $column_settings );

					// Get row_number - this determines column order.
					$final_row_number = isset( $column_data['row_number'] ) ? absint( $column_data['row_number'] ) : 1;

					// Add column to columns_group rows.
					$addon_data['general'][ $section_id ]['row_groups'][ $group_id ]['columns_groups'][ $columns_id ]['rows'][ $row_id ] = array(
						'row_number'      => $final_row_number,
						'column_settings' => $column_settings,
						'fields'          => array(),
					);
				}
			}
		}

		// Parse fields.
		if ( ! empty( $post_data['epofw_field'] ) ) {
			foreach ( $post_data['epofw_field'] as $field_id => $field_data ) {
				$field = self::parse_single_field( $field_id, $field_data );

				$section_id = isset( $field_data['section_id'] ) && ! empty( $field_data['section_id'] ) ? $field_data['section_id'] : '';

				if ( empty( $section_id ) && ! empty( $addon_data['general'] ) ) {
					$section_id = array_key_first( $addon_data['general'] );
				}

				if ( ! empty( $section_id ) && ! isset( $addon_data['general'][ $section_id ] ) ) {
					$addon_data['general'][ $section_id ] = array(
						'section_id'       => $section_id,
						'section_title'    => 'Section ' . $section_id,
						'section_status'   => 'on',
						'section_order'    => 1,
						'section_settings' => array(),
						'fields'           => array(),
					);
				}

				if ( empty( $section_id ) ) {
					continue;
				}

				$addon_data['general'][ $section_id ]['fields'][ $field_id ] = $field;
			}
		}

		// Reorganize fields from section level to row_groups structure based on row_association.
		if ( ! empty( $addon_data['general'] ) ) {
			foreach ( $addon_data['general'] as $section_id => &$section_data ) {
				if ( empty( $section_data['fields'] ) || ! is_array( $section_data['fields'] ) ) {
					continue;
				}

				foreach ( $section_data['fields'] as $field_id => $field_data ) {
					if ( ! is_array( $field_data ) || ! isset( $field_data['row_association'] ) ) {
						continue;
					}

					$row_assoc  = $field_data['row_association'];
					$group_id   = isset( $row_assoc['group_id'] ) ? $row_assoc['group_id'] : '';
					$row_id     = isset( $row_assoc['row_id'] ) ? $row_assoc['row_id'] : '';
					$columns_id = isset( $row_assoc['columns_id'] ) ? $row_assoc['columns_id'] : null;

					if ( empty( $group_id ) || empty( $row_id ) ) {
						continue;
					}

					// Ensure row_groups structure exists.
					if ( ! isset( $section_data['row_groups'] ) ) {
						$section_data['row_groups'] = array();
					}
					if ( ! isset( $section_data['row_groups'][ $group_id ] ) ) {
						$section_data['row_groups'][ $group_id ] = array(
							'row_group_settings' => array(),
							'columns_groups'     => array(),
						);
					}

					// If columns_id is provided, use new structure.
					if ( $columns_id ) {
						// Ensure columns_groups structure exists.
						if ( ! isset( $section_data['row_groups'][ $group_id ]['columns_groups'][ $columns_id ] ) ) {
							$columns_order = isset( $row_assoc['columns_order'] ) ? absint( $row_assoc['columns_order'] ) : 0;
							$layout_type   = isset( $row_assoc['layout'] ) ? sanitize_text_field( $row_assoc['layout'] ) : 'single';

							$section_data['row_groups'][ $group_id ]['columns_groups'][ $columns_id ] = array(
								'columns_order' => $columns_order,
								'layout_type'   => $layout_type,
								'columns_count' => 0,
								'rows'          => array(),
							);
						}

						// Ensure row exists in columns_group.
						if ( ! isset( $section_data['row_groups'][ $group_id ]['columns_groups'][ $columns_id ]['rows'][ $row_id ] ) ) {
							$section_data['row_groups'][ $group_id ]['columns_groups'][ $columns_id ]['rows'][ $row_id ] = array(
								'row_number'      => 1,
								'column_settings' => array(),
								'fields'          => array(),
							);
						}

						// Add field to the row.
						$section_data['row_groups'][ $group_id ]['columns_groups'][ $columns_id ]['rows'][ $row_id ]['fields'][ $field_id ] = $field_data;
					} else {
						// Legacy: no columns_id, use rows directly (will be migrated to default columns_group during save).
						if ( ! isset( $section_data['row_groups'][ $group_id ]['rows'] ) ) {
							$section_data['row_groups'][ $group_id ]['rows'] = array();
						}
						if ( ! isset( $section_data['row_groups'][ $group_id ]['rows'][ $row_id ] ) ) {
							$section_data['row_groups'][ $group_id ]['rows'][ $row_id ] = array(
								'row_number'      => 1,
								'column_settings' => array(),
								'fields'          => array(),
							);
						}

						// Add field to the row.
						$section_data['row_groups'][ $group_id ]['rows'][ $row_id ]['fields'][ $field_id ] = $field_data;
					}
				}

				// Remove fields from section level (they're now in row_groups).
				unset( $section_data['fields'] );
			}
		}

		// Parse additional rules.
		// Support both old flat format and new nested format (backward compatible).
		$addon_data['additional_rule_data'] = array();

		// Check if new nested format is being submitted.
		if ( ! empty( $post_data['additional_rule_data']['groups'] ) && is_array( $post_data['additional_rule_data']['groups'] ) ) {
			// New nested format.
			$addon_data['additional_rule_data'] = array(
				'structure_version'    => '2.0',
				'groups'               => array(),
				'logic_between_groups' => isset( $post_data['additional_rule_data']['logic_between_groups'] ) ? sanitize_text_field( $post_data['additional_rule_data']['logic_between_groups'] ) : 'and',
			);

			foreach ( $post_data['additional_rule_data']['groups'] as $group_index => $group_data ) {
				if ( ! is_array( $group_data ) || empty( $group_data['conditions'] ) ) {
					continue;
				}

				$group = array(
					'id'         => isset( $group_data['id'] ) ? sanitize_text_field( $group_data['id'] ) : 'group_' . wp_rand( 100000, 999999 ),
					'logic'      => isset( $group_data['logic'] ) ? sanitize_text_field( wp_unslash( $group_data['logic'] ) ) : 'and',
					'conditions' => array(),
				);

				// Debug: Log the group logic being saved.

				foreach ( $group_data['conditions'] as $condition_index => $condition_data ) {
					if ( ! is_array( $condition_data ) || empty( $condition_data['condition'] ) ) {
						continue;
					}

					$condition = array(
						'condition' => sanitize_text_field( $condition_data['condition'] ),
						'operator'  => isset( $condition_data['operator'] ) ? sanitize_text_field( $condition_data['operator'] ) : 'is_equal_to',
						'value'     => array(),
					);

					if ( ! empty( $condition_data['value'] ) && is_array( $condition_data['value'] ) ) {
						$condition['value'] = array_map( 'sanitize_text_field', $condition_data['value'] );
					} elseif ( ! empty( $condition_data['value'] ) ) {
						$condition['value'] = array( sanitize_text_field( $condition_data['value'] ) );
					}

					$group['conditions'][] = $condition;
				}

				if ( ! empty( $group['conditions'] ) ) {
					$addon_data['additional_rule_data']['groups'][] = $group;
				}
			}

			/**
			 * Filter to modify rule data before saving.
			 *
			 * @since 4.4.4
			 * @param array $addon_data['additional_rule_data'] Rule data.
			 */
			$addon_data['additional_rule_data'] = apply_filters( 'epofw_pre_save_additional_rules', $addon_data['additional_rule_data'] );
		} else {
			// Old flat format (backward compatibility).
			// Merge both direct format (condition, operator) and nested epofw_data format (value).
			// The form uses different name patterns for different fields:
			// - additional_rule_data[id][condition] and additional_rule_data[id][operator] (direct)
			// - epofw_data[additional_rule_data][id][value][] (nested in epofw_data).
			$addon_data['additional_rule_data'] = array();

			// First, get condition and operator from direct format.
			if ( ! empty( $post_data['additional_rule_data'] ) && is_array( $post_data['additional_rule_data'] ) ) {
				foreach ( $post_data['additional_rule_data'] as $rule_id => $rule_data ) {
					if ( 'groups' === $rule_id || 'logic_between_groups' === $rule_id || 'structure_version' === $rule_id ) {
						continue; // Skip nested format keys in old format parsing.
					}
					$addon_data['additional_rule_data'][ $rule_id ] = $rule_data;
				}
			}

			// Then, merge in values from nested epofw_data format.
			if ( ! empty( $post_data['epofw_data']['additional_rule_data'] ) && is_array( $post_data['epofw_data']['additional_rule_data'] ) ) {
				foreach ( $post_data['epofw_data']['additional_rule_data'] as $rule_id => $rule_data ) {
					if ( ! isset( $addon_data['additional_rule_data'][ $rule_id ] ) ) {
						$addon_data['additional_rule_data'][ $rule_id ] = array();
					}
					// Merge all fields (condition, operator, value) from nested format.
					if ( isset( $rule_data['condition'] ) ) {
						$addon_data['additional_rule_data'][ $rule_id ]['condition'] = $rule_data['condition'];
					}
					if ( isset( $rule_data['operator'] ) ) {
						$addon_data['additional_rule_data'][ $rule_id ]['operator'] = $rule_data['operator'];
					}
					if ( isset( $rule_data['value'] ) ) {
						$addon_data['additional_rule_data'][ $rule_id ]['value'] = $rule_data['value'];
					}
				}
			}
		}

		// Remove empty additional_rule_data if no rules exist.
		if ( empty( $addon_data['additional_rule_data'] ) || ( isset( $addon_data['additional_rule_data']['groups'] ) && empty( $addon_data['additional_rule_data']['groups'] ) ) ) {
			unset( $addon_data['additional_rule_data'] );
		}

		return $addon_data;
	}

	/**
	 * Parse a single field
	 *
	 * @since 4.4.0
	 * @param string $field_id   Field ID.
	 * @param array  $field_data Field data from POST.
	 * @return array Normalized field structure.
	 */
	private static function parse_single_field( $field_id, $field_data ) {
		$field = array(
			'field_id'     => sanitize_text_field( $field_id ),
			'field_status' => isset( $field_data['field_status'] ) ? sanitize_text_field( $field_data['field_status'] ) : 'on',
			'field_order'  => isset( $field_data['order'] ) ? absint( $field_data['order'] ) : 0,
			'section_id'   => isset( $field_data['section_id'] ) ? sanitize_text_field( $field_data['section_id'] ) : '',
		);

		// Field type and basic properties.
		if ( isset( $field_data['field'] ) && is_array( $field_data['field'] ) ) {
			$field['field'] = $field_data['field'];

			// Map default_value to value for all field types that support it.
			// This ensures the value attribute is set correctly in the database.
			if ( isset( $field['field']['default_value'] ) && ! empty( $field['field']['default_value'] ) ) {
				// Only set value if it's not already set (allows override if needed).
				if ( ! isset( $field['field']['value'] ) || empty( $field['field']['value'] ) ) {
					$field['field']['value'] = $field['field']['default_value'];
				}
				// Remove default_value from field data as it's not needed in the database.
				unset( $field['field']['default_value'] );
			}

			// Ensure name and id are set. If empty, use epofw_field_ prefix with field_id.
			// This ensures cart/frontend can always match fields correctly.
			if ( empty( $field['field']['name'] ) ) {
				$field['field']['name'] = 'epofw_field_' . $field_id;
			}
			if ( empty( $field['field']['id'] ) ) {
				$field['field']['id'] = 'epofw_field_' . $field_id;
			}
		}

		// Label data.
		if ( isset( $field_data['label'] ) && is_array( $field_data['label'] ) ) {
			$field['label'] = $field_data['label'];
		}

		// Row association (v2.3: includes columns_id and columns_order).
		$field['row_association'] = array(
			'section_id'    => isset( $field_data['section_id'] ) ? sanitize_text_field( $field_data['section_id'] ) : '',
			'row_id'        => isset( $field_data['row_id'] ) ? sanitize_text_field( $field_data['row_id'] ) : '',
			'group_id'      => isset( $field_data['group_id'] ) ? sanitize_text_field( $field_data['group_id'] ) : '',
			'columns_id'    => isset( $field_data['columns_id'] ) ? sanitize_text_field( $field_data['columns_id'] ) : null,
			'columns_order' => isset( $field_data['columns_order'] ) ? absint( $field_data['columns_order'] ) : 0,
			'layout'        => isset( $field_data['layout'] ) ? sanitize_text_field( $field_data['layout'] ) : 'single',
			'columns'       => isset( $field_data['columns'] ) ? absint( $field_data['columns'] ) : 1,
		);

		// Field order.
		if ( isset( $field_data['order'] ) ) {
			$field['field_order'] = absint( $field_data['order'] );
		}

		// Complex nested structures (already in correct format from JSON).
		$complex_keys = array(
			'epofw_field_settings',
			'input_container_settings',
			'field_container_settings',
			'separator_settings',
			'logical_operation',
			'apodp_logical_operation',
			'common',
			'repeater_settings',
		);

		foreach ( $complex_keys as $key ) {
			if ( isset( $field_data[ $key ] ) && is_array( $field_data[ $key ] ) ) {
				$field[ $key ] = $field_data[ $key ];
			}
		}

		return $field;
	}

	/**
	 * Filter row group settings to keep only layout properties.
	 *
	 * Removes style-related settings (background, border, padding, margin, etc.),
	 * keeps only layout properties (gap, alignment).
	 *
	 * @since 4.4.0
	 * @param array $group_data Raw row group settings from POST.
	 * @return array Filtered settings with only layout properties.
	 */
	private static function filter_row_group_settings( $group_data ) {
		if ( ! is_array( $group_data ) ) {
			return array();
		}

		$filtered = array();

		// Keep layout properties only.
		if ( isset( $group_data['gap'] ) ) {
			$filtered['gap'] = $group_data['gap'];
		}
		if ( isset( $group_data['alignment'] ) ) {
			$filtered['alignment'] = $group_data['alignment'];
		}

		// Remove style-related keys.
		$style_keys = array(
			'enabled',
			'background_color',
			'border_color',
			'border_width',
			'border_style',
			'border_radius',
			'padding',
			'margin',
			'custom_css',
		);

		// Note: We intentionally don't include style keys in filtered array.
		return $filtered;
	}

	/**
	 * Filter column settings - columns are layout-only (no styling).
	 *
	 * Removes all style-related settings from column settings.
	 *
	 * @since 4.4.0
	 * @param array $column_settings Raw column settings from POST.
	 * @return array Empty array (columns are layout-only).
	 */
	private static function filter_column_settings( $column_settings ) {
		// Columns are layout-only, no settings to save.
		return array();
	}
}
