<?php
/**
 * Responsive element stack order helpers.
 *
 * @package JOCBM
 */

namespace JOCBM\Services;

defined( 'ABSPATH' ) || exit;

/**
 * Desktop / tablet / mobile content-stack order → CSS `order`.
 */
final class ElementsOrder {

	public const DEFAULTS = array( 'badge', 'header', 'subheader', 'image', 'logos', 'buttons' );

	/**
	 * @param mixed                $value    Flat list or map.
	 * @param array<int, string>   $defaults Allowed keys / default order.
	 * @return array{desktop: array<int, string>, tablet: ?array<int, string>, mobile: ?array<int, string>}
	 */
	public static function normalize( $value, array $defaults = self::DEFAULTS ): array {
		$defaults = self::resolve_list( $defaults, self::DEFAULTS );

		if ( self::is_list( $value ) ) {
			return array(
				'desktop' => self::resolve_list( $value, $defaults ),
				'tablet'  => null,
				'mobile'  => null,
			);
		}

		if ( is_array( $value ) && ( isset( $value['desktop'] ) || isset( $value['tablet'] ) || isset( $value['mobile'] ) ) ) {
			$desktop = self::resolve_list( $value['desktop'] ?? null, $defaults );
			$tablet  = self::is_list( $value['tablet'] ?? null ) ? self::resolve_list( $value['tablet'], $defaults ) : null;
			$mobile  = self::is_list( $value['mobile'] ?? null ) ? self::resolve_list( $value['mobile'], $defaults ) : null;
			return array(
				'desktop' => $desktop,
				'tablet'  => $tablet,
				'mobile'  => $mobile,
			);
		}

		return array(
			'desktop' => $defaults,
			'tablet'  => null,
			'mobile'  => null,
		);
	}

	/**
	 * @param mixed              $value      Value.
	 * @param string             $breakpoint desktop|tablet|mobile.
	 * @param array<int, string> $defaults   Defaults.
	 * @return array<int, string>
	 */
	public static function effective( $value, string $breakpoint = 'desktop', array $defaults = self::DEFAULTS ): array {
		$n   = self::normalize( $value, $defaults );
		$out = $n['desktop'];
		if ( ( 'tablet' === $breakpoint || 'mobile' === $breakpoint ) && is_array( $n['tablet'] ) ) {
			$out = $n['tablet'];
		}
		if ( 'mobile' === $breakpoint && is_array( $n['mobile'] ) ) {
			$out = $n['mobile'];
		}
		return $out;
	}

	/**
	 * Sanitize stored value (legacy list or breakpoint map).
	 *
	 * @param mixed              $value    Raw.
	 * @param array<int, string> $defaults Defaults.
	 * @return array{desktop: array<int, string>, tablet?: array<int, string>, mobile?: array<int, string>}
	 */
	public static function sanitize( $value, array $defaults = self::DEFAULTS ): array {
		$n   = self::normalize( $value, $defaults );
		$out = array( 'desktop' => $n['desktop'] );
		if ( is_array( $n['tablet'] ) ) {
			$out['tablet'] = $n['tablet'];
		}
		if ( is_array( $n['mobile'] ) ) {
			$out['mobile'] = $n['mobile'];
		}
		return $out;
	}

	/**
	 * @param string             $selector CSS selector.
	 * @param mixed              $value    Order value.
	 * @param array<int, string> $defaults Defaults.
	 */
	public static function to_css( string $selector, $value, array $defaults = self::DEFAULTS ): string {
		if ( '' === $selector ) {
			return '';
		}
		// If author didn't request any explicit ordering, we can rely on
		// DOM insertion order (the markup stack is already built in the
		// correct default order during interpolation).
		if ( null === $value || ( is_array( $value ) && empty( $value ) ) ) {
			return '';
		}
		if ( self::is_redundant_default_order( $value, $defaults ) ) {
			return '';
		}
		$n       = self::normalize( $value, $defaults );
		$parts   = array( self::order_rules( $selector, $n['desktop'] ) );
		$tablet  = self::effective( $n, 'tablet', $defaults );
		$mobile  = self::effective( $n, 'mobile', $defaults );
		if ( $tablet !== $n['desktop'] ) {
			$parts[] = '@media (max-width: 1024px){' . self::order_rules( $selector, $tablet ) . '}';
		}
		if ( $mobile !== $tablet ) {
			$parts[] = '@media (max-width: 480px){' . self::order_rules( $selector, $mobile ) . '}';
		}
		return implode( "\n", array_filter( $parts ) );
	}

	/**
	 * Block-level + per-slide order CSS.
	 *
	 * @param string               $block_selector Selector.
	 * @param array<string, mixed> $attrs          Block attrs.
	 * @param array<int, string>   $defaults       Defaults.
	 */
	public static function to_css_for_attrs( string $block_selector, array $attrs, array $defaults = self::DEFAULTS ): string {
		if ( '' === $block_selector ) {
			return '';
		}
		$parts = array();
		// Always emit (same as JS): unset elementsOrder still gets default stack order.
		$css = self::to_css( $block_selector, $attrs['elementsOrder'] ?? null, $defaults );
		if ( '' !== $css ) {
			$parts[] = $css;
		}
		$slides = isset( $attrs['slides'] ) && is_array( $attrs['slides'] ) ? $attrs['slides'] : array();
		foreach ( $slides as $index => $slide ) {
			if ( ! is_array( $slide ) ) {
				continue;
			}
			$sel = $block_selector . ' [data-bm-slide-index="' . (int) $index . '"]';
			$css = self::to_css( $sel, $slide['elementsOrder'] ?? null, $defaults );
			if ( '' !== $css ) {
				$parts[] = $css;
			}
		}
		return implode( "\n\n", $parts );
	}

	/**
	 * Prefer global default stack, then any extra partial keys (markup order).
	 *
	 * @param array<int, string> $partial_keys Keys present in markup.
	 * @return array<int, string>
	 */
	public static function defaults_for_partials( array $partial_keys ): array {
		$available = array();
		foreach ( $partial_keys as $key ) {
			if ( is_string( $key ) && '' !== $key ) {
				$available[] = sanitize_key( $key );
			}
		}
		$available = array_values( array_unique( array_filter( $available ) ) );
		if ( ! $available ) {
			return self::DEFAULTS;
		}
		$ordered = array();
		foreach ( self::DEFAULTS as $key ) {
			if ( in_array( $key, $available, true ) ) {
				$ordered[] = $key;
			}
		}
		foreach ( $available as $key ) {
			if ( ! in_array( $key, $ordered, true ) ) {
				$ordered[] = $key;
			}
		}
		return $ordered;
	}

	/**
	 * Resolve block-specific elementsOrder defaults from field schema/default attrs.
	 * Mirrors the JS fallback chain so server-rendered CSS only emits when needed.
	 *
	 * @param array<int, array<string, mixed>> $field_schema  Field schema.
	 * @param array<string, mixed>             $default_attrs Default attrs.
	 * @return array<int, string>
	 */
	public static function defaults_from_schema( array $field_schema = array(), array $default_attrs = array() ): array {
		foreach ( $field_schema as $field ) {
			if ( ! is_array( $field ) || 'elementsOrder' !== ( $field['type'] ?? '' ) ) {
				continue;
			}
			if ( ! empty( $field['default'] ) && self::is_list( $field['default'] ) ) {
				return self::resolve_list( $field['default'], self::DEFAULTS );
			}
			break;
		}

		$from_attrs = $default_attrs['elementsOrder'] ?? null;
		if ( self::is_list( $from_attrs ) ) {
			return self::resolve_list( $from_attrs, self::DEFAULTS );
		}
		if ( is_array( $from_attrs ) && self::is_list( $from_attrs['desktop'] ?? null ) ) {
			return self::resolve_list( $from_attrs['desktop'], self::DEFAULTS );
		}

		$styleable_keys = array();
		foreach ( $field_schema as $field ) {
			if ( ! is_array( $field ) || empty( $field['elementStyle'] ) || empty( $field['key'] ) || ! is_string( $field['key'] ) ) {
				continue;
			}
			$styleable_keys[] = sanitize_key( $field['key'] );
		}
		$styleable_keys = array_values( array_unique( array_filter( $styleable_keys ) ) );
		if ( $styleable_keys ) {
			return $styleable_keys;
		}

		return self::DEFAULTS;
	}

	/**
	 * Flat list for DOM contentStack (always desktop).
	 *
	 * @param mixed              $value    Value.
	 * @param array<int, string> $defaults Defaults.
	 * @return array<int, string>
	 */
	public static function for_dom( $value, array $defaults = self::DEFAULTS ): array {
		return self::effective( $value, 'desktop', $defaults );
	}

	/**
	 * @param mixed $value Value.
	 */
	private static function is_list( $value ): bool {
		if ( ! is_array( $value ) || array() === $value ) {
			return false;
		}
		return array_keys( $value ) === range( 0, count( $value ) - 1 );
	}

	/**
	 * @param mixed              $value    Value.
	 * @param array<int, string> $defaults Defaults.
	 * @return array<int, string>
	 */
	private static function resolve_list( $value, array $defaults ): array {
		$base = array();
		foreach ( $defaults as $key ) {
			if ( is_string( $key ) && '' !== $key ) {
				$base[] = sanitize_key( $key );
			}
		}
		if ( ! $base ) {
			$base = self::DEFAULTS;
		}
		$next = array();
		if ( self::is_list( $value ) ) {
			foreach ( $value as $key ) {
				if ( ! is_string( $key ) || '' === $key ) {
					continue;
				}
				$key = sanitize_key( $key );
				// Keep authoring order, including card keys not in hero defaults.
				if ( '' !== $key && ! in_array( $key, $next, true ) ) {
					$next[] = $key;
				}
			}
		}
		if ( ! $next ) {
			return self::collapse_order_groups( $base );
		}
		foreach ( $base as $key ) {
			if ( ! in_array( $key, $next, true ) ) {
				$next[] = $key;
			}
		}
		return self::collapse_order_groups( $next );
	}

	/**
	 * Skip CSS when stored order only repeats the default desktop stack.
	 *
	 * @param mixed              $value    Raw order value.
	 * @param array<int, string> $defaults Defaults for this block/layout.
	 */
	private static function is_redundant_default_order( $value, array $defaults ): bool {
		$n = self::normalize( $value, $defaults );
		if ( is_array( $n['tablet'] ) || is_array( $n['mobile'] ) ) {
			return false;
		}
		return $n['desktop'] === self::resolve_list( $defaults, self::DEFAULTS );
	}

	/**
	 * Collapse nested testimonial meta keys to a single `meta` stack item.
	 *
	 * @param array<int, string> $order Order.
	 * @return array<int, string>
	 */
	private static function collapse_order_groups( array $order ): array {
		$list = array();
		foreach ( $order as $key ) {
			if ( ! is_string( $key ) || '' === $key ) {
				continue;
			}
			$key = sanitize_key( $key );
			if ( '' !== $key ) {
				$list[] = $key;
			}
		}
		$needs_meta = false;
		foreach ( $list as $key ) {
			if ( in_array( $key, array( 'meta', 'name', 'company' ), true ) ) {
				$needs_meta = true;
				break;
			}
		}
		if ( ! $needs_meta ) {
			return $list;
		}
		$out       = array();
		$seen_meta = false;
		foreach ( $list as $key ) {
			if ( in_array( $key, array( 'meta', 'image', 'name', 'company' ), true ) ) {
				if ( ! $seen_meta ) {
					$out[]     = 'meta';
					$seen_meta = true;
				}
				continue;
			}
			$out[] = $key;
		}
		return $out;
	}

	/**
	 * @param string             $selector Selector.
	 * @param array<int, string> $order    Order.
	 */
	private static function order_rules( string $selector, array $order ): string {
		$parts     = array();
		$collapsed = self::collapse_order_groups( $order );
		foreach ( array_values( $collapsed ) as $i => $key ) {
			$safe = preg_replace( '/[^a-zA-Z0-9_-]/', '', (string) $key ) ?? '';
			if ( '' === $safe ) {
				continue;
			}
			$parts[] = $selector . ' [data-bm-el="' . $safe . '"]{order:' . ( $i + 1 ) . '}';
		}
		if ( in_array( 'meta', $collapsed, true ) ) {
			$parts[] = $selector . ' .bm-testimonial__meta [data-bm-el]{order:unset}';
			// Image alignment uses order on the avatar flex item (incl. hit-wrap).
			$parts[] = $selector . ' .bm-testimonial__meta > .bm-testimonial__avatar,' .
				$selector . ' .bm-testimonial__meta > .bm-el-hit,' .
				$selector . ' .bm-testimonial__meta > .bm-media-frame' .
				'{order:var(--bm-author-image-order, 0)}';
		}
		return implode( '', $parts );
	}
}
