<?php
/**
 * Built-in form Design Studio settings.
 *
 * @package SubmissionInbox
 */

namespace SUBIN\Forms;

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

/**
 * Reads and sanitizes saved Design Studio settings.
 */
class DesignSettings {
	/**
	 * Get default Design Studio structure.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public function get_defaults() {
		$defaults = array();
		foreach ( $this->get_allowed_parts() as $part ) {
			$defaults[ $part ] = array();
		}

		return $defaults;
	}

	/**
	 * Get allowed design parts.
	 *
	 * @return array<int,string>
	 */
	public function get_allowed_parts() {
		return array(
			'form_wrapper',
			'form',
			'layout_container',
			'layout_row',
			'layout_column',
			'header',
			'header_title',
			'header_description',
			'field_group',
			'field_label',
			'field_input',
			'field_help',
			'design_block',
			'submit_button',
			'footer',
			'privacy_text',
			'success_message',
			'error_message',
		);
	}

	/**
	 * Get allowed design properties.
	 *
	 * @return array<int,string>
	 */
	public function get_allowed_properties() {
		return array(
			'background_color',
			'background_gradient',
			'background_image_url',
			'background_size',
			'background_position',
			'background_repeat',
			'overlay_color',
			'overlay_opacity',
			'text_color',
			'border_color',
			'border_width',
			'border_radius',
			'padding',
			'padding_x',
			'padding_y',
			'margin_bottom',
			'font_size',
			'font_weight',
			'line_height',
			'width',
			'max_width',
			'text_align',
			'box_shadow',
			'shadow_x',
			'shadow_y',
			'shadow_blur',
			'shadow_spread',
			'shadow_color',
			'shadow_opacity',
			'hover_background_color',
			'hover_text_color',
			'hover_border_color',
			'hover_box_shadow',
			'focus_background_color',
			'focus_text_color',
			'focus_border_color',
			'focus_box_shadow',
			'error_background_color',
			'error_text_color',
			'error_border_color',
			'error_box_shadow',
			'button_width',
			'placeholder_color',
		);
	}

	/**
	 * Get sanitized Design Studio settings for a form row.
	 *
	 * @param array<string,mixed> $form Form row.
	 * @return array<string,array<string,mixed>>
	 */
	public function get_for_form( array $form ) {
		$settings = array();

		if ( ! empty( $form['settings_json'] ) && is_string( $form['settings_json'] ) ) {
			$decoded = json_decode( $form['settings_json'], true );
			if ( is_array( $decoded ) ) {
				$settings = $decoded;
			}
		}

		return $this->get_for_settings( $settings );
	}

	/**
	 * Get sanitized Design Studio settings from an already decoded settings array.
	 *
	 * @param array<string,mixed> $settings Decoded form settings.
	 * @return array<string,array<string,mixed>>
	 */
	public function get_for_settings( array $settings ) {
		$design = is_array( $settings['design_studio'] ?? null ) ? $settings['design_studio'] : array();

		return $this->merge_with_defaults( $this->sanitize( $design ) );
	}

	/**
	 * Sanitize a full Design Studio settings array.
	 *
	 * @param array<string,mixed> $design Design settings.
	 * @return array<string,array<string,mixed>>
	 */
	public function sanitize( array $design ) {
		$clean              = array();
		$allowed_parts      = $this->get_allowed_parts();
		$allowed_properties = $this->get_allowed_properties();

		foreach ( $design as $part => $settings ) {
			$part = sanitize_key( (string) $part );
			if ( ! in_array( $part, $allowed_parts, true ) || ! is_array( $settings ) ) {
				continue;
			}

			foreach ( $settings as $property => $value ) {
				$property = sanitize_key( (string) $property );
				if ( ! in_array( $property, $allowed_properties, true ) ) {
					continue;
				}

				$value = $this->sanitize_property_value( $property, $value );
				if ( '' === $value || null === $value ) {
					continue;
				}

				$clean[ $part ][ $property ] = $value;
			}
		}

		return $clean;
	}

	/**
	 * Merge sanitized settings with the default part structure.
	 *
	 * @param array<string,array<string,mixed>> $design Design settings.
	 * @return array<string,array<string,mixed>>
	 */
	public function merge_with_defaults( array $design ) {
		$defaults = $this->get_defaults();

		foreach ( $design as $part => $settings ) {
			if ( array_key_exists( $part, $defaults ) && is_array( $settings ) ) {
				$defaults[ $part ] = $settings;
			}
		}

		return $defaults;
	}

	/**
	 * Return a sanitized design array with one part updated.
	 *
	 * @param array<string,mixed> $design Existing design settings.
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<string,array<string,mixed>>
	 */
	public function update_part_settings( array $design, $part, array $settings ) {
		$design = $this->merge_with_defaults( $this->sanitize( $design ) );
		$part   = sanitize_key( $part );

		if ( ! in_array( $part, $this->get_allowed_parts(), true ) ) {
			return $design;
		}

		$updated         = $this->sanitize( array( $part => $settings ) );
		$design[ $part ] = $updated[ $part ] ?? array();

		return $design;
	}

	/**
	 * Sanitize per-field Design Studio styles.
	 *
	 * @param array<string,mixed> $field_styles Raw field style settings.
	 * @return array<int,array<string,array<string,mixed>>>
	 */
	public function sanitize_field_styles( array $field_styles ) {
		$clean = array();
		foreach ( $field_styles as $field_id => $part_settings ) {
			$field_id = absint( $field_id );
			if ( $field_id <= 0 || ! is_array( $part_settings ) ) {
				continue;
			}

			foreach ( $part_settings as $part => $settings ) {
				$part = sanitize_key( (string) $part );
				if ( ! $this->is_field_style_part( $part ) || ! is_array( $settings ) ) {
					continue;
				}

				$sanitized = $this->sanitize( array( $part => $settings ) );
				if ( ! empty( $sanitized[ $part ] ) ) {
					$clean[ $field_id ][ $part ] = $sanitized[ $part ];
				}
			}
		}

		return $clean;
	}

	/**
	 * Return sanitized per-field styles with one field part updated.
	 *
	 * @param array<string,mixed> $field_styles Existing field styles.
	 * @param int                 $field_id Field id.
	 * @param string              $part Part key.
	 * @param array<string,mixed> $settings Part settings.
	 * @return array<int,array<string,array<string,mixed>>>
	 */
	public function update_field_part_settings( array $field_styles, $field_id, $part, array $settings ) {
		$field_styles = $this->sanitize_field_styles( $field_styles );
		$field_id     = absint( $field_id );
		$part         = sanitize_key( $part );

		if ( $field_id <= 0 || ! $this->is_field_style_part( $part ) ) {
			return $field_styles;
		}

		$updated = $this->sanitize( array( $part => $settings ) );
		if ( empty( $updated[ $part ] ) ) {
			unset( $field_styles[ $field_id ][ $part ] );
			if ( empty( $field_styles[ $field_id ] ) ) {
				unset( $field_styles[ $field_id ] );
			}
			return $field_styles;
		}

		$field_styles[ $field_id ][ $part ] = $updated[ $part ];

		return $field_styles;
	}

	/**
	 * Whether a design part can be scoped to one built-in field.
	 *
	 * @param string $part Part key.
	 * @return bool
	 */
	public function is_field_style_part( $part ) {
		return in_array( sanitize_key( $part ), array( 'field_group', 'field_label', 'field_input', 'field_help' ), true );
	}

	/**
	 * Sanitize one supported property value.
	 *
	 * @param string $property Property key.
	 * @param mixed  $value Raw value.
	 * @return mixed
	 */
	private function sanitize_property_value( $property, $value ) {
		if ( in_array( $property, array( 'background_color', 'overlay_color', 'text_color', 'border_color', 'shadow_color', 'hover_background_color', 'hover_text_color', 'hover_border_color', 'focus_background_color', 'focus_text_color', 'focus_border_color', 'error_background_color', 'error_text_color', 'error_border_color', 'placeholder_color' ), true ) ) {
			return $this->sanitize_hex_color( $value );
		}

		if ( 'background_gradient' === $property ) {
			return $this->sanitize_gradient( $value );
		}

		if ( 'background_image_url' === $property ) {
			return $this->sanitize_media_url( $value );
		}

		if ( in_array( $property, array( 'border_width', 'border_radius', 'padding', 'padding_x', 'padding_y', 'margin_bottom', 'font_size', 'max_width', 'shadow_x', 'shadow_y', 'shadow_blur', 'shadow_spread' ), true ) ) {
			return $this->sanitize_pixel_value( $property, $value );
		}

		if ( in_array( $property, array( 'overlay_opacity', 'shadow_opacity' ), true ) ) {
			return $this->sanitize_percent_value( $value );
		}

		if ( 'font_weight' === $property ) {
			$value = (string) absint( $value );
			return in_array( $value, array( '300', '400', '500', '600', '700', '800' ), true ) ? $value : '';
		}

		if ( 'line_height' === $property ) {
			if ( ! is_numeric( $value ) ) {
				return '';
			}

			$value = (float) $value;
			if ( $value < 1.0 || $value > 2.2 ) {
				return '';
			}

			return (string) round( $value, 2 );
		}

		if ( 'text_align' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'left', 'center', 'right' ), true ) ? $value : '';
		}

		if ( 'width' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'auto', 'full', '25', '33', '50', '66', '75', '100' ), true ) ? $value : '';
		}

		if ( 'button_width' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'auto', 'full' ), true ) ? $value : '';
		}

		if ( 'box_shadow' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'none', 'soft', 'medium', 'strong', 'custom' ), true ) ? $value : '';
		}

		if ( in_array( $property, array( 'hover_box_shadow', 'focus_box_shadow', 'error_box_shadow' ), true ) ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'none', 'soft', 'medium', 'strong' ), true ) ? $value : '';
		}

		if ( 'background_size' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'cover', 'contain', 'auto' ), true ) ? $value : '';
		}

		if ( 'background_position' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'center_center', 'center_top', 'center_bottom', 'left_center', 'right_center' ), true ) ? $value : '';
		}

		if ( 'background_repeat' === $property ) {
			$value = sanitize_key( (string) $value );
			return in_array( $value, array( 'no-repeat', 'repeat', 'repeat-x', 'repeat-y' ), true ) ? $value : '';
		}

		return '';
	}

	/**
	 * Sanitize a WordPress media URL for background-image usage.
	 *
	 * @param mixed $value Raw value.
	 * @return string
	 */
	private function sanitize_media_url( $value ) {
		$value = trim( (string) $value );
		if ( '' === $value || preg_match( '/[\r\n\'"<>]/', $value ) ) {
			return '';
		}

		$url = esc_url_raw( $value, array( 'http', 'https' ) );
		if ( '' === $url || preg_match( '/^(?:javascript|data):/i', $url ) ) {
			return '';
		}

		return $url;
	}

	/**
	 * Sanitize a hex color.
	 *
	 * @param mixed $value Raw value.
	 * @return string
	 */
	private function sanitize_hex_color( $value ) {
		$value = trim( (string) $value );
		if ( '' === $value ) {
			return '';
		}

		return preg_match( '/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/', $value ) ? strtolower( $value ) : '';
	}

	/**
	 * Sanitize a structured linear gradient value.
	 *
	 * @param mixed $value Raw value.
	 * @return array<string,string>|string
	 */
	private function sanitize_gradient( $value ) {
		if ( ! is_array( $value ) ) {
			return '';
		}

		$type      = sanitize_key( (string) ( $value['type'] ?? '' ) );
		$direction = sanitize_key( (string) ( $value['direction'] ?? '' ) );
		$from      = $this->sanitize_hex_color( $value['from'] ?? '' );
		$to        = $this->sanitize_hex_color( $value['to'] ?? '' );

		if ( 'linear' !== $type || ! in_array( $direction, array( '90deg', '120deg', '135deg', '160deg', '180deg' ), true ) || '' === $from || '' === $to ) {
			return '';
		}

		return array(
			'type'      => 'linear',
			'direction' => $direction,
			'from'      => $from,
			'to'        => $to,
		);
	}

	/**
	 * Sanitize a numeric pixel value.
	 *
	 * @param string $property Property key.
	 * @param mixed  $value Raw value.
	 * @return int|string
	 */
	private function sanitize_pixel_value( $property, $value ) {
		if ( '' === $value || ! is_numeric( $value ) ) {
			return '';
		}

		$value  = in_array( $property, array( 'shadow_x', 'shadow_y', 'shadow_spread' ), true ) ? (int) $value : absint( $value );
		$ranges = array(
			'border_width'  => array( 0, 10 ),
			'border_radius' => array( 0, 80 ),
			'padding'       => array( 0, 120 ),
			'padding_x'     => array( 0, 120 ),
			'padding_y'     => array( 0, 120 ),
			'margin_bottom' => array( 0, 120 ),
			'font_size'     => array( 10, 64 ),
			'max_width'     => array( 240, 1400 ),
			'shadow_x'      => array( -80, 80 ),
			'shadow_y'      => array( -80, 80 ),
			'shadow_blur'   => array( 0, 160 ),
			'shadow_spread' => array( -80, 80 ),
		);

		$range = $ranges[ $property ] ?? array( 0, 120 );
		if ( $value < $range[0] || $value > $range[1] ) {
			return '';
		}

		return $value;
	}

	/**
	 * Sanitize a 0-100 percent value.
	 *
	 * @param mixed $value Raw value.
	 * @return int|string
	 */
	private function sanitize_percent_value( $value ) {
		if ( '' === $value || ! is_numeric( $value ) ) {
			return '';
		}

		$value = absint( $value );
		return $value <= 100 ? $value : '';
	}
}
