<?php
/**
 * Sanitize, validate, and minify merchant custom CSS.
 *
 * @package SKYNSMFA_Smart_Favorites
 */

declare( strict_types=1 );

namespace SKYNSMFA\Wishlist;

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

/**
 * Custom CSS helper for Button Customizer settings.
 */
final class Css_Sanitizer {
	/**
	 * Maximum stored CSS length (bytes).
	 */
	private const MAX_LENGTH = 32768;

	/**
	 * Disallowed patterns (case-insensitive).
	 *
	 * @var list<string>
	 */
	private const BLOCKED_PATTERNS = array(
		'@import',
		'@charset',
		'@namespace',
		'javascript:',
		'expression\s*\(',
		'behavior\s*:',
		'-moz-binding',
		'</style',
		'<script',
		'<\?php',
		'<%',
		'data:text/html',
		'url\s*\(\s*["\']?\s*javascript:',
	);

	/**
	 * Validate input, sanitize unsafe content, and return storable CSS.
	 *
	 * @param string $css Raw CSS from settings form.
	 * @return array{success:bool,css:string,message:string}
	 */
	public static function prepare_for_save( string $css ): array {
		$css = wp_check_invalid_utf8( $css );
		$css = str_replace( array( "\r\n", "\r" ), "\n", $css );
		$css = trim( $css );

		if ( '' === $css ) {
			return array(
				'success' => true,
				'css'     => '',
				'message' => '',
			);
		}

		if ( strlen( $css ) > self::MAX_LENGTH ) {
			return array(
				'success' => false,
				'css'     => '',
				'message' => sprintf(
					/* translators: %d: maximum CSS length in characters */
					__( 'Custom CSS was not saved: maximum length is %d characters.', 'skynet-smart-favorites' ),
					self::MAX_LENGTH
				),
			);
		}

		$blocked = self::find_blocked_pattern( $css );
		if ( null !== $blocked ) {
			return array(
				'success' => false,
				'css'     => '',
				'message' => __( 'Custom CSS was not saved: disallowed syntax or unsafe rules were detected.', 'skynet-smart-favorites' ),
			);
		}

		$css = wp_strip_all_tags( $css );
		$css = str_replace( array( '&gt;', '&lt;' ), array( '>', '<' ), $css );

		if ( ! self::has_balanced_delimiters( $css ) ) {
			return array(
				'success' => false,
				'css'     => '',
				'message' => __( 'Custom CSS was not saved: braces, parentheses, or brackets are not balanced.', 'skynet-smart-favorites' ),
			);
		}

		return array(
			'success' => true,
			'css'     => $css,
			'message' => '',
		);
	}

	/**
	 * Minify CSS for frontend inline output.
	 *
	 * @param string $css Sanitized CSS.
	 * @return string
	 */
	public static function minify( string $css ): string {
		$css = trim( $css );
		if ( '' === $css ) {
			return '';
		}

		$css = preg_replace( '!/\*[^*]*\*+([^/][^*]*\*+)*/!', '', $css );
		if ( ! is_string( $css ) ) {
			return '';
		}

		$css = preg_replace( '/\s+/', ' ', $css );
		$css = preg_replace( '/\s*{\s*/', '{', $css );
		$css = preg_replace( '/\s*}\s*/', '}', $css );
		$css = preg_replace( '/\s*;\s*/', ';', $css );
		$css = preg_replace( '/\s*:\s*/', ':', $css );
		$css = preg_replace( '/\s*,\s*/', ',', $css );

		return trim( (string) $css );
	}

	/**
	 * Find the first blocked pattern in CSS.
	 *
	 * @param string $css CSS string.
	 * @return string|null Matched pattern label.
	 */
	private static function find_blocked_pattern( string $css ): ?string {
		foreach ( self::BLOCKED_PATTERNS as $pattern ) {
			if ( 1 === preg_match( '/' . $pattern . '/i', $css ) ) {
				return $pattern;
			}
		}

		return null;
	}

	/**
	 * Check balanced {}, (), and [] outside strings and comments.
	 *
	 * @param string $css CSS string.
	 * @return bool
	 */
	private static function has_balanced_delimiters( string $css ): bool {
		$brace_depth   = 0;
		$paren_depth   = 0;
		$bracket_depth = 0;
		$length        = strlen( $css );
		$in_string     = false;
		$string_quote  = '';
		$in_comment    = false;
		$escaped       = false;

		for ( $i = 0; $i < $length; $i++ ) {
			$char      = $css[ $i ];
			$next_char = ( $i + 1 < $length ) ? $css[ $i + 1 ] : '';

			if ( $in_comment ) {
				if ( '*' === $char && '/' === $next_char ) {
					$in_comment = false;
					++$i;
				}
				continue;
			}

			if ( $in_string ) {
				if ( $escaped ) {
					$escaped = false;
					continue;
				}
				if ( '\\' === $char ) {
					$escaped = true;
					continue;
				}
				if ( $char === $string_quote ) {
					$in_string    = false;
					$string_quote = '';
				}
				continue;
			}

			if ( '/' === $char && '*' === $next_char ) {
				$in_comment = true;
				++$i;
				continue;
			}

			if ( '"' === $char || "'" === $char ) {
				$in_string    = true;
				$string_quote = $char;
				continue;
			}

			switch ( $char ) {
				case '{':
					++$brace_depth;
					break;
				case '}':
					--$brace_depth;
					break;
				case '(':
					++$paren_depth;
					break;
				case ')':
					--$paren_depth;
					break;
				case '[':
					++$bracket_depth;
					break;
				case ']':
					--$bracket_depth;
					break;
			}

			if ( $brace_depth < 0 || $paren_depth < 0 || $bracket_depth < 0 ) {
				return false;
			}
		}

		return ! $in_string && ! $in_comment && 0 === $brace_depth && 0 === $paren_depth && 0 === $bracket_depth;
	}
}
