<?php
/**
 * Pending recording taxonomy terms.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Recording;

/**
 * Stores taxonomy terms that should be created only when a recording executes.
 */
final class RecordingPendingTermStore {
	public const TOKEN_PREFIX     = 'scheduled-changes-pending-term-';
	private const TERMS_META      = '_scheduled_changes_recording_pending_terms';
	private const ASSIGNMENT_META = '_scheduled_changes_recording_pending_term_assignments';

	/**
	 * Creates a shadow-local pending term token.
	 *
	 * @return array<string,mixed>
	 */
	public function create(
		int $shadow_product_id,
		string $taxonomy,
		string $name,
		int $parent_term_id = 0,
		string $parent_token = ''
	): array {
		$taxonomy_object = get_taxonomy( $taxonomy );

		if ( ! $taxonomy_object instanceof \WP_Taxonomy ) {
			throw new \RuntimeException( 'Taxonomy is not available for recording.' );
		}

		$name = trim( sanitize_text_field( $name ) );

		if ( '' === $name ) {
			throw new \RuntimeException( 'Term name is required.' );
		}

		$token = self::TOKEN_PREFIX . str_replace( '-', '', wp_generate_uuid4() );
		$term  = array(
			'token'          => $token,
			'taxonomy'       => $taxonomy,
			'name'           => $name,
			'slug'           => sanitize_title( $name ),
			'parent_term_id' => max( 0, $parent_term_id ),
			'parent_token'   => $this->isToken( $parent_token ) ? $parent_token : '',
			'created_at_utc' => gmdate( 'c' ),
		);

		$terms           = $this->terms( $shadow_product_id );
		$terms[ $token ] = $term;
		update_post_meta( $shadow_product_id, self::TERMS_META, $terms );

		return $term;
	}

	/**
	 * Normalizes a submitted taxonomy assignment into existing term IDs and pending tokens.
	 *
	 * @param array<int|string,mixed> $submitted Submitted term values.
	 * @return array<int,int|string>
	 */
	public function normalizeAssignment( int $shadow_product_id, string $taxonomy, array $submitted ): array {
		$terms      = $this->terms( $shadow_product_id );
		$normalized = array();

		foreach ( $submitted as $candidate ) {
			$value = is_scalar( $candidate ) ? trim( (string) $candidate ) : '';

			if ( '' === $value ) {
				continue;
			}

			if ( $this->isPlaceholderTermId( $value ) ) {
				continue;
			}

			if ( isset( $terms[ $value ] ) && $taxonomy === (string) $terms[ $value ]['taxonomy'] ) {
				$normalized[] = $value;
				continue;
			}

			if ( ctype_digit( $value ) && (int) $value > 0 && term_exists( (int) $value, $taxonomy ) ) {
				$normalized[] = (int) $value;
				continue;
			}

			$existing = term_exists( $value, $taxonomy );

			if ( is_array( $existing ) && (int) ( $existing['term_id'] ?? 0 ) > 0 ) {
				$normalized[] = (int) $existing['term_id'];
				continue;
			}

			if ( false !== $existing && null !== $existing && is_numeric( $existing ) ) {
				$normalized[] = (int) $existing;
				continue;
			}

			$term         = $this->create( $shadow_product_id, $taxonomy, $value );
			$normalized[] = (string) $term['token'];
			$terms        = $this->terms( $shadow_product_id );
		}

		return $this->uniqueAssignment( $normalized );
	}

	private function isPlaceholderTermId( string $value ): bool {
		$integer = filter_var( $value, FILTER_VALIDATE_INT );

		return false !== $integer && $integer <= 0;
	}

	/**
	 * Persists the selected mixed assignment for a taxonomy.
	 *
	 * @param array<int,int|string> $assignment Existing term IDs and pending tokens.
	 */
	public function setAssignment( int $shadow_product_id, string $taxonomy, array $assignment ): void {
		$assignments              = $this->assignments( $shadow_product_id );
		$assignments[ $taxonomy ] = $this->uniqueAssignment( $assignment );

		if ( ! $this->containsToken( $assignments[ $taxonomy ] ) ) {
			unset( $assignments[ $taxonomy ] );
		}

		update_post_meta( $shadow_product_id, self::ASSIGNMENT_META, $assignments );
	}

	/**
	 * Returns pending term data.
	 *
	 * @return array<string,array<string,mixed>>
	 */
	public function terms( int $shadow_product_id ): array {
		$value = get_post_meta( $shadow_product_id, self::TERMS_META, true );

		return is_array( $value ) ? $value : array();
	}

	/**
	 * Returns mixed taxonomy assignments.
	 *
	 * @return array<string,array<int,int|string>>
	 */
	public function assignments( int $shadow_product_id ): array {
		$value = get_post_meta( $shadow_product_id, self::ASSIGNMENT_META, true );

		return is_array( $value ) ? $value : array();
	}

	/**
	 * Returns all data needed by recording snapshots.
	 *
	 * @return array<string,mixed>
	 */
	public function snapshot( int $shadow_product_id ): array {
		return array(
			'terms'       => $this->terms( $shadow_product_id ),
			'assignments' => $this->assignments( $shadow_product_id ),
		);
	}

	public function isToken( string $value ): bool {
		return strncmp( $value, self::TOKEN_PREFIX, strlen( self::TOKEN_PREFIX ) ) === 0;
	}

	/**
	 * @param array<int,int|string> $assignment Existing term IDs and pending tokens.
	 */
	private function containsToken( array $assignment ): bool {
		foreach ( $assignment as $value ) {
			if ( is_string( $value ) && $this->isToken( $value ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * @param array<int,int|string> $assignment Existing term IDs and pending tokens.
	 * @return array<int,int|string>
	 */
	private function uniqueAssignment( array $assignment ): array {
		$unique = array();

		foreach ( $assignment as $value ) {
			$key = is_int( $value ) ? 'id:' . $value : 'token:' . $value;

			if ( is_int( $value ) && $value > 0 ) {
				$unique[ $key ] = $value;
			} elseif ( is_string( $value ) && $this->isToken( $value ) ) {
				$unique[ $key ] = $value;
			}
		}

		return array_values( $unique );
	}
}
