<?php
/**
 * Variable Reference Validator
 *
 * Validates variable references in workflow action configurations.
 *
 * @package Sequensy\Validation
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Validation;

use Sequensy\Validation\Contracts\ValidatorInterface;
use Sequensy\Validation\Concerns\CollectsErrors;

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

/**
 * Class VariableReferenceValidator
 *
 * Validates that variable references in action configs reference valid fields.
 */
class VariableReferenceValidator implements ValidatorInterface {

	use CollectsErrors;

	/**
	 * Trigger registry instance.
	 *
	 * @var object
	 */
	private object $trigger_registry;

	/**
	 * Action registry instance.
	 *
	 * @var object
	 */
	private object $action_registry;

	/**
	 * Action validator for label resolution.
	 *
	 * @var ActionValidator
	 */
	private ActionValidator $action_validator;

	/**
	 * Constructor.
	 *
	 * @param object          $trigger_registry Trigger registry instance.
	 * @param object          $action_registry  Action registry instance.
	 * @param ActionValidator $action_validator Action validator for label resolution.
	 */
	public function __construct(
		object $trigger_registry,
		object $action_registry,
		ActionValidator $action_validator
	) {
		$this->trigger_registry = $trigger_registry;
		$this->action_registry  = $action_registry;
		$this->action_validator = $action_validator;
	}

	/**
	 * Validate variable references in workflow configuration.
	 *
	 * @param array $config  Workflow configuration.
	 * @param array $context Additional context (unused).
	 * @return array Validation result.
	 */
	public function validate( array $config, array $context = array() ): array {
		$this->clear_errors();

		$actions = $config['actions'] ?? array();

		if ( empty( $actions ) ) {
			return $this->get_result();
		}

		foreach ( $actions as $index => $action ) {
			$action_label  = $this->action_validator->get_action_label( $action, $index );
			$action_config = isset( $action['config'] ) ? $action['config'] : array();

			// Build available fields for this action (trigger + all preceding actions).
			$available = $this->build_available_fields_map( $config, $index );

			// Check each config value for variable references.
			foreach ( $action_config as $field_name => $value ) {
				$field_name = (string) $field_name;
				$references = $this->extract_variable_references( $value );

				foreach ( $references as $reference ) {
					$this->validate_variable_reference(
						$reference,
						$available,
						sprintf( '%s field "%s"', $action_label, $field_name ),
						$index,
						$field_name
					);
				}
			}
		}

		return $this->get_result();
	}

	/**
	 * Extract all variable references from a value.
	 *
	 * @param mixed $value Value to search for variables.
	 * @return array Array of variable references found (without braces).
	 */
	public function extract_variable_references( mixed $value ): array {
		$references = array();

		if ( is_string( $value ) ) {
			// Match variable references in double-brace format.
			preg_match_all( '/\{\{([^}]+)\}\}/', $value, $matches );
			if ( ! empty( $matches[1] ) ) {
				$references = array_merge( $references, $matches[1] );
			}
		} elseif ( is_array( $value ) ) {
			// Recursively search arrays.
			foreach ( $value as $item ) {
				$references = array_merge( $references, $this->extract_variable_references( $item ) );
			}
		}

		return $references;
	}

	/**
	 * Build map of available fields from trigger and preceding actions.
	 *
	 * @param array      $config       Workflow configuration.
	 * @param int|string $action_index Current action index (fields from actions at/after this are not available).
	 * @return array Map of namespace => array of field names.
	 */
	public function build_available_fields_map( array $config, int|string $action_index ): array {
		$available = array();

		// Add trigger fields.
		if ( isset( $config['trigger']['type'] ) ) {
			$trigger_handler = $this->trigger_registry->get_trigger( $config['trigger']['type'] );
			if ( $trigger_handler && method_exists( $trigger_handler, 'get_available_fields' ) ) {
				$fields               = $trigger_handler->get_available_fields();
				$available['Trigger'] = array_column( $fields, 'name' );
			}
		}

		// Add fields from preceding actions (by their label or type as fallback).
		$actions = $config['actions'] ?? array();
		if ( ! empty( $actions ) ) {
			foreach ( $actions as $key => $preceding_action ) {
				// Stop when we reach the current action.
				if ( $key === $action_index ) {
					break;
				}

				// Actions must have a type to be referenced.
				if ( empty( $preceding_action['type'] ) ) {
					continue;
				}

				// Use label if set, otherwise fall back to action type.
				// Must sanitize to match frontend and WorkflowExecutor namespace format.
				$raw_namespace = ! empty( $preceding_action['label'] )
					? $preceding_action['label']
					: $preceding_action['type'];
				$namespace     = $this->sanitize_namespace( $raw_namespace );

				$action_handler = $this->action_registry->get_action( $preceding_action['type'] );
				if ( $action_handler ) {
					$output_fields           = $action_handler->get_output_fields();
					$available[ $namespace ] = array_column( $output_fields, 'name' );
				}
			}
		}

		return $available;
	}

	/**
	 * Sanitize a namespace label to match WorkflowExecutor format.
	 *
	 * Must stay in sync with WorkflowExecutor::sanitize_namespace()
	 * and admin/src/utils/namespace.ts::sanitizeNamespace().
	 *
	 * @param string $label The label to sanitize.
	 * @return string Sanitized namespace key.
	 */
	private function sanitize_namespace( string $label ): string {
		$result = strtolower( $label );
		$result = preg_replace( '/[^a-z0-9]+/', '_', $result );
		$result = trim( $result, '_' );
		return $result;
	}

	/**
	 * Validate a single variable reference.
	 *
	 * @param string     $reference    Variable reference (e.g., "Trigger.user_email").
	 * @param array      $available    Available fields map.
	 * @param string     $context      Context description for error message.
	 * @param int|string $action_index Action index.
	 * @param string     $field_name   Field name in action config.
	 * @return void
	 */
	private function validate_variable_reference(
		string $reference,
		array $available,
		string $context,
		int|string $action_index,
		string $field_name
	): void {
		// Parse reference: "Namespace.field" or "Namespace.nested.path".
		$parts = explode( '.', $reference, 2 );

		if ( count( $parts ) < 2 ) {
			$this->add_error(
				'invalid_variable_format',
				sprintf(
					'Invalid variable format "{{%s}}" in %s. Expected format: {{Namespace.field}}',
					$reference,
					$context
				),
				array(
					'action_index' => $action_index,
					'field'        => $field_name,
					'reference'    => $reference,
				)
			);
			return;
		}

		$namespace = trim( $parts[0] );
		$field     = trim( $parts[1] );

		// Handle nested paths by extracting root field.
		$field_parts = explode( '.', $field );
		$root_field  = $field_parts[0];

		// Check namespace exists.
		if ( ! isset( $available[ $namespace ] ) ) {
			$available_namespaces = array_keys( $available );
			$this->add_error(
				'unknown_variable_namespace',
				sprintf(
					'Unknown variable source "%s" in %s. Available sources: %s',
					$namespace,
					$context,
					! empty( $available_namespaces ) ? implode( ', ', $available_namespaces ) : 'none'
				),
				array(
					'action_index'         => $action_index,
					'field'                => $field_name,
					'reference'            => $reference,
					'namespace'            => $namespace,
					'available_namespaces' => $available_namespaces,
				)
			);
			return;
		}

		// Check root field exists in namespace.
		if ( ! in_array( $root_field, $available[ $namespace ], true ) ) {
			$this->add_error(
				'unknown_variable_field',
				sprintf(
					'Unknown field "%s" in %s namespace (used in %s). Available fields: %s',
					$root_field,
					$namespace,
					$context,
					! empty( $available[ $namespace ] ) ? implode( ', ', $available[ $namespace ] ) : 'none'
				),
				array(
					'action_index'     => $action_index,
					'field'            => $field_name,
					'reference'        => $reference,
					'namespace'        => $namespace,
					'referenced_field' => $root_field,
					'available_fields' => $available[ $namespace ],
				)
			);
		}
	}
}
