<?php
/**
 * JSON helpers — always UTF-8 (no \uXXXX) so WordPress stripslashes cannot corrupt text.
 *
 * @package AuraBuilder
 */

declare(strict_types=1);

namespace AuraBuilder\Support;

final class Json {

	/**
	 * Encode for nested settings (items, images, etc.).
	 * JSON_UNESCAPED_UNICODE keeps "matrícula" and 📍 as real UTF-8.
	 * Avoids: json_encode → "\u00ed" → stripslashes → "u00ed" mojibake.
	 *
	 * @param mixed $data
	 */
	public static function encode( $data, string $empty = '[]' ): string {
		$flags = 0;
		if ( defined( 'JSON_UNESCAPED_UNICODE' ) ) {
			$flags |= JSON_UNESCAPED_UNICODE;
		}
		if ( defined( 'JSON_UNESCAPED_SLASHES' ) ) {
			$flags |= JSON_UNESCAPED_SLASHES;
		}
		// Prefer wp_json_encode when available (WP will still honor flags in recent versions).
		if ( function_exists( 'wp_json_encode' ) ) {
			$json = wp_json_encode( $data, $flags );
		} else {
			$json = json_encode( $data, $flags ); // phpcs:ignore WordPress.WP.AlternativeFunctions
		}
		return is_string( $json ) && $json !== '' ? $json : $empty;
	}

	/**
	 * Repair strings damaged by stripslashes on \uXXXX sequences:
	 * "matru00edcula" → "matrícula", "ud83dudccd" → 📍
	 */
	public static function repair_stripped_unicode( string $s ): string {
		if ( $s === '' || ( ! str_contains( $s, 'u00' ) && ! preg_match( '/u[dD][89abABc-fC-F]/', $s ) ) ) {
			return $s;
		}
		// Surrogate pairs (emoji) first: ud83dudccd
		$s = (string) preg_replace_callback(
			'/u([dD][89abAB][0-9a-fA-F]{2})u([dD][c-fC-F][0-9a-fA-F]{2})/',
			static function ( array $m ): string {
				$hi = hexdec( $m[1] );
				$lo = hexdec( $m[2] );
				$code = ( $hi - 0xD800 ) * 0x400 + ( $lo - 0xDC00 ) + 0x10000;
				if ( $code < 0x10000 || $code > 0x10FFFF ) {
					return $m[0];
				}
				return self::utf8_chr( $code );
			},
			$s
		);
		// BMP escapes: u00ed, u2014, u00e7, u00e1…
		$s = (string) preg_replace_callback(
			'/u([0-9a-fA-F]{4})/',
			static function ( array $m ): string {
				$code = hexdec( $m[1] );
				// Only rewrite plausible unicode-escape leftovers (latin, punct, symbols).
				if (
					( $code >= 0x00A0 && $code <= 0x024F )
					|| ( $code >= 0x1E00 && $code <= 0x1EFF )
					|| ( $code >= 0x2000 && $code <= 0x206F )
					|| ( $code >= 0x20A0 && $code <= 0x20CF )
					|| ( $code >= 0x2100 && $code <= 0x214F )
					|| ( $code >= 0x2190 && $code <= 0x21FF )
					|| ( $code >= 0x2600 && $code <= 0x27BF )
				) {
					return self::utf8_chr( $code );
				}
				return $m[0];
			},
			$s
		);
		return $s;
	}

	private static function utf8_chr( int $code ): string {
		if ( $code < 0x80 ) {
			return chr( $code );
		}
		if ( function_exists( 'mb_chr' ) ) {
			$ch = mb_chr( $code, 'UTF-8' );
			return is_string( $ch ) ? $ch : '';
		}
		if ( $code < 0x800 ) {
			return chr( 0xC0 | ( $code >> 6 ) ) . chr( 0x80 | ( $code & 0x3F ) );
		}
		if ( $code < 0x10000 ) {
			return chr( 0xE0 | ( $code >> 12 ) )
				. chr( 0x80 | ( ( $code >> 6 ) & 0x3F ) )
				. chr( 0x80 | ( $code & 0x3F ) );
		}
		return chr( 0xF0 | ( $code >> 18 ) )
			. chr( 0x80 | ( ( $code >> 12 ) & 0x3F ) )
			. chr( 0x80 | ( ( $code >> 6 ) & 0x3F ) )
			. chr( 0x80 | ( $code & 0x3F ) );
	}
}
