<?php
/**
 * Sanitize inbound document JSON (security + schema).
 *
 * @package AuraBuilder
 */

declare(strict_types=1);

namespace AuraBuilder\Document;

use AuraBuilder\Widgets\WidgetRegistry;

final class DocumentSanitizer {

	/**
	 * @param array<string, mixed> $raw
	 */
	public static function sanitize( array $raw ): Document {
		$doc = new Document( $raw );
		$doc->settings = Document::normalize_settings( $doc->settings );

		$allowed_types = WidgetRegistry::instance()->types();
		$allowed_types[] = 'root';

		$clean_nodes = array();
		foreach ( $doc->nodes as $id => $node ) {
			if ( ! in_array( $node->type, $allowed_types, true ) ) {
				continue;
			}
			$widget = WidgetRegistry::instance()->get( $node->type );
			if ( $widget ) {
				// Root included — RootWidget keeps content_width / max_width / gap.
				$node->settings = $widget->sanitize_settings( $node->settings );
			} else {
				$node->settings = self::sanitize_generic_settings( $node->settings );
			}
			// Styles: only scalar/array leafs, keys sanitized.
			$node->styles = self::sanitize_styles( $node->styles );
			$clean_nodes[ $id ] = $node;
		}

		// Drop children pointing to missing nodes.
		foreach ( $clean_nodes as $node ) {
			$node->children = array_values(
				array_filter(
					$node->children,
					static fn( string $cid ) => isset( $clean_nodes[ $cid ] )
				)
			);
			if ( $node->parent_id && ! isset( $clean_nodes[ $node->parent_id ] ) ) {
				$node->parent_id = null;
			}
		}

		$doc->nodes = $clean_nodes;
		if ( ! isset( $doc->nodes[ $doc->root_id ] ) ) {
			// Recreate empty if corrupted.
			return Document::empty_page( $doc->title );
		}

		return $doc;
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	private static function sanitize_generic_settings( array $settings ): array {
		$out = array();
		foreach ( $settings as $k => $v ) {
			$key = sanitize_key( (string) $k );
			if ( is_string( $v ) ) {
				$out[ $key ] = sanitize_text_field( $v );
			} elseif ( is_int( $v ) || is_float( $v ) || is_bool( $v ) ) {
				$out[ $key ] = $v;
			} elseif ( is_array( $v ) ) {
				$out[ $key ] = self::sanitize_generic_settings( $v );
			}
		}
		return $out;
	}

	/**
	 * @param array<string, mixed> $styles
	 * @return array{desktop?:array<string,mixed>,tablet?:array<string,mixed>,mobile?:array<string,mixed>}
	 */
	private static function sanitize_styles( array $styles ): array {
		$out = array(
			'desktop' => array(),
			'tablet'  => array(),
			'mobile'  => array(),
		);

		foreach ( array( 'desktop', 'tablet', 'mobile' ) as $bp ) {
			// Accept both object {} and list from broken clients.
			$bag = $styles[ $bp ] ?? null;
			if ( is_object( $bag ) ) {
				$bag = (array) $bag;
			}
			if ( ! is_array( $bag ) ) {
				continue;
			}
			foreach ( $bag as $k => $v ) {
				$key = sanitize_key( (string) $k );
				if ( $key === '' ) {
					continue;
				}

				// Visibility flag.
				if ( $key === 'hide' ) {
					if ( $v === true || $v === 1 || $v === '1' || $v === 'true' ) {
						$out[ $bp ][ $key ] = true;
					}
					continue;
				}

				if ( is_bool( $v ) ) {
					$out[ $bp ][ $key ] = $v;
					continue;
				}

				if ( is_int( $v ) || is_float( $v ) ) {
					$out[ $bp ][ $key ] = $v;
					continue;
				}

				if ( ! is_string( $v ) ) {
					continue;
				}

				$v = trim( $v );
				if ( $v === '' ) {
					continue;
				}

				// Hex color: #rgb #rrggbb #rrggbbaa
				if ( preg_match( '/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/', $v ) ) {
					$out[ $bp ][ $key ] = strtolower( $v );
					continue;
				}

				// Background image URL
				if ( $key === 'bg_image' || ( str_starts_with( $v, 'http://' ) || str_starts_with( $v, 'https://' ) || str_starts_with( $v, '/' ) ) ) {
					if ( $key === 'bg_image' || in_array( $key, array( 'bg_image', 'background_image' ), true ) ) {
						$url = esc_url_raw( $v );
						if ( $url !== '' ) {
							$out[ $bp ]['bg_image'] = $url;
						}
						continue;
					}
				}

				// rgb/rgba/hsl/hsla
				if ( preg_match( '/^(rgb|rgba|hsl|hsla)\(\s*[\d.%,\s\/]+\)$/i', $v ) ) {
					$out[ $bp ][ $key ] = $v;
					continue;
				}

				// CSS gradients: linear-gradient(...) / radial-gradient(...)
				if ( $key === 'bg_gradient' && preg_match( '/^(linear|radial)-gradient\s*\(/i', $v ) ) {
					$clean = preg_replace( '/[^\w\s\-#.,%()\/"\':+]/', '', $v );
					if ( is_string( $clean ) && $clean !== '' ) {
						$out[ $bp ][ $key ] = $clean;
					}
					continue;
				}

				// Gradient meta: type, kind, angle, stops JSON
				if ( $key === 'bg_type' ) {
					$k = sanitize_key( $v );
					if ( in_array( $k, array( 'solid', 'gradient' ), true ) ) {
						$out[ $bp ][ $key ] = $k;
					}
					continue;
				}
				if ( $key === 'bg_grad_kind' ) {
					$k = sanitize_key( $v );
					if ( in_array( $k, array( 'linear', 'radial' ), true ) ) {
						$out[ $bp ][ $key ] = $k;
					}
					continue;
				}
				if ( $key === 'bg_grad_angle' && is_numeric( $v ) ) {
					$out[ $bp ][ $key ] = (string) max( 0, min( 360, (int) $v ) );
					continue;
				}
				if ( $key === 'bg_grad_stops' && str_starts_with( $v, '[' ) ) {
					$decoded = json_decode( $v, true );
					if ( is_array( $decoded ) ) {
						$clean = array();
						foreach ( $decoded as $row ) {
							if ( ! is_array( $row ) ) {
								continue;
							}
							$c = trim( (string) ( $row['c'] ?? $row['color'] ?? '' ) );
							$p = isset( $row['p'] ) ? (int) $row['p'] : ( isset( $row['pos'] ) ? (int) $row['pos'] : 0 );
							if ( $c === '' || ! preg_match( '/^(#([0-9a-fA-F]{3,8})|rgba?\([^)]+\)|hsla?\([^)]+\))$/', $c ) ) {
								continue;
							}
							$clean[] = array(
								'c' => $c,
								'p' => max( 0, min( 100, $p ) ),
							);
						}
						if ( count( $clean ) >= 2 ) {
							$json = wp_json_encode( $clean );
							if ( is_string( $json ) ) {
								$out[ $bp ][ $key ] = $json;
							}
						}
					}
					continue;
				}

				// CSS variables: var(--token)
				if ( preg_match( '/^var\(\s*--[a-zA-Z0-9_-]+\s*(?:,\s*[^)]+)?\)$/', $v ) ) {
					$out[ $bp ][ $key ] = $v;
					continue;
				}

				// Keywords for bg_size / position / repeat
				if ( in_array( $key, array( 'bg_size', 'bg_position', 'bg_repeat', 'bg_attachment' ), true ) ) {
					$out[ $bp ][ $key ] = sanitize_text_field( $v );
					continue;
				}

				// Generic safe CSS length/keyword values (width, margin, font-size…).
				$clean = preg_replace( '/[^\w\s\-#.,%()\/"\':]/', '', $v );
				if ( is_string( $clean ) && $clean !== '' ) {
					$out[ $bp ][ $key ] = $clean;
				}
			}
		}

		return $out;
	}
}
