<?php
/**
 * Variable Replacer Class
 *
 * Handles template variable replacement in action configurations.
 * Supports namespaced {{Namespace.field}} syntax with dot-notation nested access.
 *
 * Extracted from WorkflowExecutor to be independently testable and reusable.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Engine;

// If this file is called directly, abort.
if ( ! defined( 'WPINC' ) ) {
	die;
}

/**
 * Class VariableReplacer
 *
 * Replaces {{Namespace.field}} template variables in action config arrays
 * using namespaced execution context data.
 */
final class VariableReplacer {

	/**
	 * Replace template variables in an action config array.
	 *
	 * Recursively processes the config, replacing {{Namespace.field}} patterns
	 * in string values using the provided context.
	 *
	 * @param array<string, mixed> $config  Action configuration to process.
	 * @param array<string, mixed> $context Namespaced context data for variable replacement.
	 * @return array<string, mixed> Config with variables replaced.
	 */
	public function replace( array $config, array $context ): array {
		return $this->replace_in_array( $config, $context );
	}

	/**
	 * Recursively replace variables in an array.
	 *
	 * Iterates through array values, replacing strings and recursing into
	 * nested arrays. Non-string, non-array values pass through unchanged.
	 *
	 * @param array<string, mixed> $data    Data to process.
	 * @param array<string, mixed> $context Namespaced context data.
	 * @return array<string, mixed> Processed data.
	 */
	private function replace_in_array( array $data, array $context ): array {
		foreach ( $data as $key => $item ) {
			if ( is_string( $item ) ) {
				$data[ $key ] = $this->replace_in_string( $item, $context );
			} elseif ( is_array( $item ) ) {
				$data[ $key ] = $this->replace_in_array( $item, $context );
			}
		}

		return $data;
	}

	/**
	 * Replace all {{variable}} patterns in a string.
	 *
	 * @param string               $value   String containing variable placeholders.
	 * @param array<string, mixed> $context Namespaced context data.
	 * @return string String with variables replaced.
	 */
	private function replace_in_string( string $value, array $context ): string {
		return preg_replace_callback(
			'/\{\{([^}]+)\}\}/',
			function ( $matches ) use ( $context ) {
				$variable = trim( $matches[1] );
				return $this->resolve( $variable, $context );
			},
			$value
		);
	}

	/**
	 * Resolve a variable path to its value from namespaced context.
	 *
	 * Variable paths must be in format: Namespace.field or Namespace.field.nested
	 * where Namespace is 'Trigger' or an action label.
	 *
	 * @param string               $reference Variable path (e.g., "Trigger.user_email", "create_post.post_id").
	 * @param array<string, mixed> $context   Namespaced execution context.
	 * @return string Resolved value or empty string if not found.
	 */
	private function resolve( string $reference, array $context ): string {
		$parts = explode( '.', $reference );

		// Must have at least Namespace.field format.
		if ( count( $parts ) < 2 ) {
			return '';
		}

		// First part is always the namespace (Trigger, create_post, etc.).
		$namespace = array_shift( $parts );

		// Check if namespace exists in context.
		if ( ! isset( $context[ $namespace ] ) ) {
			return '';
		}

		// Get the remaining path within the namespace.
		$remaining_path = implode( '.', $parts );

		return $this->get_nested( $context[ $namespace ], $remaining_path );
	}

	/**
	 * Get nested value from context using dot notation.
	 *
	 * Walks the dot-separated path through nested arrays and objects,
	 * returning scalar values as strings and complex values as JSON.
	 *
	 * @param mixed  $data Context data (within a single namespace).
	 * @param string $path Key in dot notation (e.g., 'user.email' or just 'email').
	 * @return string Value or empty string if not found.
	 */
	private function get_nested( mixed $data, string $path ): string {
		$keys  = explode( '.', $path );
		$value = $data;

		foreach ( $keys as $nested_key ) {
			if ( is_array( $value ) && isset( $value[ $nested_key ] ) ) {
				$value = $value[ $nested_key ];
			} elseif ( is_object( $value ) && isset( $value->$nested_key ) ) {
				$value = $value->$nested_key;
			} else {
				return '';
			}
		}

		// Return scalar values as strings, arrays/objects as JSON.
		if ( is_scalar( $value ) ) {
			return (string) $value;
		} elseif ( is_array( $value ) || is_object( $value ) ) {
			return wp_json_encode( $value );
		}

		return '';
	}
}
