<?php
/**
 * WooCommerce product attribute codec.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\WooCommerce\Attributes;

// phpcs:disable WordPress.Security.EscapeOutput.ExceptionNotEscaped -- Exception messages are escaped at UI boundaries.

use RuntimeException;

/**
 * Converts WC_Product_Attribute objects to and from canonical arrays.
 */
final class AttributeCodec {
	/**
	 * @param array<string,mixed> $attributes WooCommerce attributes.
	 * @return array<int,array<string,mixed>>
	 */
	public function encode( array $attributes ): array {
		$encoded = array();

		foreach ( $attributes as $attribute ) {
			if ( ! $attribute instanceof \WC_Product_Attribute ) {
				continue;
			}

			$encoded[] = array(
				'id'        => $attribute->get_id(),
				'name'      => $attribute->get_name(),
				'taxonomy'  => $attribute->is_taxonomy(),
				'options'   => array_values( $attribute->get_options() ),
				'position'  => $attribute->get_position(),
				'visible'   => $attribute->get_visible(),
				'variation' => $attribute->get_variation(),
			);
		}

		usort(
			$encoded,
			static fn ( array $left, array $right ): int => (int) $left['position'] <=> (int) $right['position']
		);

		return $encoded;
	}

	/**
	 * @param array<int,array<string,mixed>> $definitions Canonical attributes.
	 * @return \WC_Product_Attribute[]
	 */
	public function decode( array $definitions, bool $allow_term_creation = false ): array {
		$attributes = array();

		foreach ( $definitions as $position => $definition ) {
			$name         = sanitize_title( (string) ( $definition['name'] ?? '' ) );
			$taxonomy     = (bool) ( $definition['taxonomy'] ?? strncmp( $name, 'pa_', strlen( 'pa_' ) ) === 0 );
			$attribute    = new \WC_Product_Attribute();
			$attribute_id = (int) ( $definition['id'] ?? 0 );

			if ( '' === $name ) {
				throw new RuntimeException( 'Attribute names are required.' );
			}

			if ( $taxonomy ) {
				$name = strncmp( $name, 'pa_', strlen( 'pa_' ) ) === 0 ? $name : wc_attribute_taxonomy_name( $name );

				if ( ! taxonomy_exists( $name ) ) {
					throw new RuntimeException( 'Unknown global attribute taxonomy: ' . $name );
				}

				if ( 0 === $attribute_id ) {
					$attribute_id = (int) wc_attribute_taxonomy_id_by_name( $name );
				}
			}

			$options = $this->decodeOptions(
				$name,
				$taxonomy,
				is_array( $definition['options'] ?? null ) ? $definition['options'] : array(),
				$allow_term_creation
			);

			$attribute->set_id( $attribute_id );
			$attribute->set_name( $name );
			$attribute->set_options( $options );
			$attribute->set_position( (int) ( $definition['position'] ?? $position ) );
			$attribute->set_visible( (bool) ( $definition['visible'] ?? true ) );
			$attribute->set_variation( (bool) ( $definition['variation'] ?? false ) );
			$attributes[] = $attribute;
		}

		return $attributes;
	}

	/**
	 * @param mixed[] $options Canonical option IDs or names.
	 * @return array<int,int|string>
	 */
	private function decodeOptions(
		string $name,
		bool $taxonomy,
		array $options,
		bool $allow_term_creation
	): array {
		if ( ! $taxonomy ) {
			return array_values(
				array_map(
					static fn ( $option ): string => (string) $option,
					$options
				)
			);
		}

		$term_ids = array();

		foreach ( $options as $option ) {
			$term = is_numeric( $option )
				? get_term( (int) $option, $name )
				: get_term_by( 'slug', sanitize_title( (string) $option ), $name );

			if ( ! $term || is_wp_error( $term ) ) {
				if ( ! $allow_term_creation || ! is_string( $option ) || '' === trim( $option ) ) {
					throw new RuntimeException( 'Missing global attribute term for ' . $name . '.' );
				}

				$created = wp_insert_term( $option, $name );

				if ( is_wp_error( $created ) ) {
					throw new RuntimeException( $created->get_error_message() );
				}

				$term_ids[] = (int) $created['term_id'];
				continue;
			}

			$term_ids[] = (int) $term->term_id;
		}

		return array_values( array_unique( $term_ids ) );
	}
}
