<?php
/**
 * Sanitize block attribute maps for storage and front-end edit.
 *
 * @package JOCBM
 */

namespace JOCBM\Services;

defined( 'ABSPATH' ) || exit;

/**
 * Class AttrSanitizer
 */
final class AttrSanitizer {

	/**
	 * Sanitize attribute map.
	 *
	 * @param array<string, mixed>                  $attrs        Attrs.
	 * @param array<int, array<string, mixed>>|null $field_schema Optional schema.
	 * @return array<string, mixed>
	 */
	public static function sanitize( array $attrs, ?array $field_schema = null ): array {
		return self::sanitize_attrs( $attrs, $field_schema );
	}

	public static function align_attrs_to_schema( array $attrs, array $field_schema ): array {
		$canonical = array();
		foreach ( $field_schema as $field ) {
			if ( empty( $field['key'] ) || ! is_string( $field['key'] ) ) {
				continue;
			}
			$canonical[ strtolower( $field['key'] ) ] = $field['key'];
		}

		if ( ! $canonical ) {
			return $attrs;
		}

		$out = array();
		foreach ( $attrs as $key => $value ) {
			$lookup = strtolower( (string) $key );
			$out[ $canonical[ $lookup ] ?? (string) $key ] = $value;
		}
		return $out;
	}

	/**
	 * Sanitize content attribute map (allows safe HTML for WYSIWYG fields).
	 * Preserves camelCase keys and nested arrays (buttons, logos, media, order).
	 *
	 * @param array<string, mixed>                  $attrs        Attrs.
	 * @param array<int, array<string, mixed>>|null $field_schema Optional schema for key alignment.
	 * @return array<string, mixed>
	 */
	public static function sanitize_attrs( array $attrs, ?array $field_schema = null ): array {
		if ( null !== $field_schema ) {
			$attrs = self::align_attrs_to_schema( $attrs, $field_schema );
		}

		$by_key = array();
		if ( is_array( $field_schema ) ) {
			foreach ( $field_schema as $field ) {
				if ( ! empty( $field['key'] ) && is_string( $field['key'] ) ) {
					$by_key[ $field['key'] ] = $field;
				}
			}
		}

		$out = array();
		foreach ( $attrs as $key => $value ) {
			// Keep camelCase — do not use sanitize_key() (it lowercases).
			$safe_key = preg_replace( '/[^a-zA-Z0-9_]/', '', (string) $key ) ?? '';
			if ( '' === $safe_key ) {
				continue;
			}
			$field              = $by_key[ $safe_key ] ?? array( 'key' => $safe_key, 'type' => 'text' );
			$out[ $safe_key ] = self::sanitize_attr_value( $value, $field );
		}
		return $out;
	}

	/**
	 * Sanitize one attribute value according to field type.
	 *
	 * @param mixed                $value Value.
	 * @param array<string, mixed> $field Field schema.
	 * @return mixed
	 */
	public static function sanitize_attr_value( $value, array $field ) {
		$type = isset( $field['type'] ) ? (string) $field['type'] : 'text';

		if ( 'repeater' === $type || 'elementsOrder' === $type ) {
			if ( ! is_array( $value ) ) {
				return array();
			}
			if ( 'elementsOrder' === $type ) {
				return ElementsOrder::sanitize( $value );
			}
			$items = array_values( $value );
			$item_fields = isset( $field['fields'] ) && is_array( $field['fields'] ) ? $field['fields'] : array();
			$out         = array();
			foreach ( $items as $item ) {
				if ( ! is_array( $item ) ) {
					continue;
				}
				$out[] = self::sanitize_repeater_item( $item, $item_fields, (string) ( $field['itemType'] ?? '' ) );
			}
			return $out;
		}

		if ( in_array( $type, array( 'media', 'image' ), true ) ) {
			return self::sanitize_media_value( is_array( $value ) ? $value : array() );
		}

		if ( 'link' === $type ) {
			return self::sanitize_link_value( is_array( $value ) ? $value : array() );
		}

		// Injected attribute (not a schema field type) — keep responsive shape.
		if ( 'elementsVisibility' === (string) ( $field['key'] ?? '' ) ) {
			return ElementsVisibility::sanitize( is_array( $value ) ? $value : array() );
		}

		$type_lower = strtolower( $type );
		$is_rich_text = in_array(
			$type_lower,
			array( 'text', 'textarea', 'wysiwyg', 'richtext', 'textwysiwyg', 'textwysywig' ),
			true
		) || null !== AllowedHtml::commands_from_field( $field );

		// Legacy features repeater (array of {text}) → HTML list items.
		if ( is_array( $value ) && $is_rich_text ) {
			$value = MarkupInterpolator::normalize_features_html( $value );
		}

		if ( is_array( $value ) ) {
			// Other maps (and legacy flat visibility until saved again).
			$out = array();
			foreach ( $value as $k => $v ) {
				$safe = preg_replace( '/[^a-zA-Z0-9_]/', '', (string) $k ) ?? '';
				if ( '' === $safe ) {
					continue;
				}
				if ( is_bool( $v ) ) {
					$out[ $safe ] = $v;
				} elseif ( is_numeric( $v ) ) {
					$out[ $safe ] = 0 + $v;
				} elseif ( is_array( $v ) ) {
					$out[ $safe ] = self::sanitize_attr_value( $v, array( 'type' => 'text' ) );
				} else {
					$out[ $safe ] = wp_kses_post( (string) $v );
				}
			}
			return $out;
		}

		if ( is_bool( $value ) ) {
			return $value;
		}
		if ( is_int( $value ) || is_float( $value ) ) {
			return $value;
		}
		if ( null === $value ) {
			return '';
		}

		$str = (string) $value;

		$plain = ! empty( $field['disableAdvanceEditor'] )
			|| ! empty( $field['disableAdvancedEditor'] )
			|| ! empty( $field['disable_advance_editor'] );

		if ( $plain ) {
			return sanitize_text_field( wp_strip_all_tags( $str ) );
		}

		// Text / WYSIWYG content — respect allowedCommand when present.
		if ( $is_rich_text ) {
			$commands = AllowedHtml::commands_from_field( $field );
			// Unrestricted rich fields still go through AllowedHtml (no media/scripts).
			return AllowedHtml::sanitize( $str, $commands );
		}

		return wp_kses_post( $str );
	}

	/**
	 * Sanitize a repeater row (button, logo, or generic).
	 *
	 * @param array<string, mixed>             $item        Item.
	 * @param array<int, array<string, mixed>> $item_fields Sub-fields.
	 * @param string                           $item_type   itemType hint.
	 * @return array<string, mixed>
	 */
	public static function sanitize_repeater_item( array $item, array $item_fields, string $item_type ): array {
		$out = array();

		if ( 'logo' === $item_type || isset( $item['image'] ) ) {
			$out['image'] = self::sanitize_media_value(
				isset( $item['image'] ) && is_array( $item['image'] ) ? $item['image'] : array()
			);
		}

		foreach ( $item_fields as $sub ) {
			if ( empty( $sub['key'] ) || ! is_string( $sub['key'] ) ) {
				continue;
			}
			$key = $sub['key'];
			if ( ! array_key_exists( $key, $item ) ) {
				continue;
			}
			$out[ $key ] = self::sanitize_attr_value( $item[ $key ], $sub );
		}

		// Preserve known button style keys that aren't in item fields.
		$scalar_keys = array(
			'styleMode',
			'libraryBlockId',
			'bg',
			'color',
			'fontFamily',
			'bgHover',
			'colorHover',
			'size',
			'padY',
			'padX',
			'borderRadius',
			'shadow',
			'shadowPreset',
			'shadowColor',
			'shadowHover',
			'shadowHoverPreset',
			'shadowHoverColor',
			'icon',
			'iconPosition',
			'iconSize',
			'iconColor',
		);
		foreach ( $scalar_keys as $key ) {
			if ( ! array_key_exists( $key, $item ) ) {
				continue;
			}
			$raw = $item[ $key ];
			if ( null === $raw ) {
				$out[ $key ] = null;
			} elseif ( is_bool( $raw ) || is_int( $raw ) || is_float( $raw ) ) {
				$out[ $key ] = $raw;
			} else {
				$out[ $key ] = sanitize_text_field( (string) $raw );
			}
		}

		if ( isset( $item['link'] ) && is_array( $item['link'] ) && ! isset( $out['link'] ) ) {
			$out['link'] = self::sanitize_link_value( $item['link'] );
		}

		// Per-slide Layout cssVars (hero slider).
		if ( 'slide' === $item_type || isset( $item['cssVars'] ) || isset( $item['layout'] ) ) {
			if ( isset( $item['cssVars'] ) && is_array( $item['cssVars'] ) ) {
				$out['cssVars'] = CssVars::normalize( $item['cssVars'] );
			}
			if ( isset( $item['layout'] ) && ! isset( $out['layout'] ) ) {
				$out['layout'] = sanitize_key( (string) $item['layout'] );
			}
			if ( isset( $item['elementsOrder'] ) && is_array( $item['elementsOrder'] ) ) {
				$out['elementsOrder'] = ElementsOrder::sanitize( $item['elementsOrder'] );
			}
			if ( isset( $item['elementsVisibility'] ) && is_array( $item['elementsVisibility'] ) ) {
				$out['elementsVisibility'] = ElementsVisibility::sanitize( $item['elementsVisibility'] );
			}
		}

		return $out;
	}

	/**
	 * @param array<string, mixed> $media Media object.
	 * @return array<string, mixed>
	 */
	public static function sanitize_media_value( array $media ): array {
		$type = isset( $media['type'] ) ? sanitize_key( (string) $media['type'] ) : 'image';
		if ( ! in_array( $type, array( 'image', 'video', 'iframe' ), true ) ) {
			$type = 'image';
		}
		return array(
			'id'   => isset( $media['id'] ) ? (int) $media['id'] : 0,
			'url'  => self::sanitize_media_url( (string) ( $media['url'] ?? '' ) ),
			'alt'  => sanitize_text_field( (string) ( $media['alt'] ?? '' ) ),
			'mime' => sanitize_text_field( (string) ( $media['mime'] ?? '' ) ),
			'type' => $type,
		);
	}

	/**
	 * Allow http(s) URLs and plugin bm-asset: paths.
	 */
	public static function sanitize_media_url( string $url ): string {
		$url = trim( $url );
		if ( '' === $url ) {
			return '';
		}
		if ( str_starts_with( $url, 'bm-asset:' ) ) {
			$rel = substr( $url, strlen( 'bm-asset:' ) );
			$rel = ltrim( str_replace( '\\', '/', $rel ), '/' );
			$rel = preg_replace( '/[^a-zA-Z0-9_\-\.\/]/', '', $rel ) ?? '';
			return $rel ? 'bm-asset:' . $rel : '';
		}
		return esc_url_raw( $url );
	}

	/**
	 * @param array<string, mixed> $link Link object.
	 * @return array{title: string, url: string, opensInNewTab: bool}
	 */
	public static function sanitize_link_value( array $link ): array {
		$raw = trim( (string) ( $link['url'] ?? '' ) );
		$url = ( '#' === $raw || str_starts_with( $raw, '#' ) )
			? sanitize_text_field( $raw )
			: esc_url_raw( $raw );
		return array(
			'title'         => sanitize_text_field( (string) ( $link['title'] ?? '' ) ),
			'url'           => $url,
			'opensInNewTab' => ! empty( $link['opensInNewTab'] ),
		);
	}
}
