<?php
/**
 * Execution Guard
 *
 * Prevents infinite loops in workflow executions by tracking active executions
 * and blocking re-entry scenarios.
 *
 * @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 ExecutionGuard
 *
 * Provides loop prevention for workflow executions through three layers:
 * 1. Same-request protection: Tracks active workflow IDs in static array
 * 2. Context deduplication: Prevents same trigger context from being processed twice
 * 3. Strict mode: Blocks any nested executions (workflows triggered by other workflows)
 */
class ExecutionGuard {

	/**
	 * Maximum execution depth allowed.
	 * With strict mode (1), no nested workflows are allowed.
	 *
	 * @var int
	 */
	public const MAX_DEPTH = 1;

	/**
	 * Active execution hashes in the current request.
	 * Key: context_hash (includes workflow_id + trigger_type + context), Value: true
	 *
	 * This tracks unique {workflow_id, trigger_type, context} combinations
	 * to prevent the same execution from running twice in the same request.
	 *
	 * @var array<string, true>
	 */
	private static array $active_executions = array();

	/**
	 * Flag indicating an action is currently being executed.
	 *
	 * When true, any new triggers should be blocked to prevent loops
	 * from actions that modify data and fire WordPress hooks.
	 *
	 * @var bool
	 */
	private static bool $action_executing = false;

	/**
	 * Check if a workflow execution can proceed.
	 *
	 * Returns false and the reason if execution should be blocked.
	 *
	 * @param int    $workflow_id  Workflow ID to execute.
	 * @param array  $context      Trigger context data.
	 * @param string $trigger_type Trigger type identifier.
	 * @return array{can_execute: bool, reason: string|null}
	 */
	public function can_execute( int $workflow_id, array $context, string $trigger_type ): array {
		// Layer 1: Block if an action is currently executing.
		// This prevents loops from actions that fire WordPress hooks.
		if ( self::$action_executing ) {
			return array(
				'can_execute' => false,
				'reason'      => 'action_executing',
			);
		}

		// Layer 2: Strict mode - block any nested executions.
		// If _sequensy_meta exists, this trigger was fired by a workflow action.
		if ( isset( $context['_sequensy_meta'] ) ) {
			return array(
				'can_execute' => false,
				'reason'      => 'nested_execution_blocked',
			);
		}

		// Layer 3: Check for duplicate execution in same request.
		// Same workflow + same trigger + same context = blocked.
		$context_hash = $this->generate_context_hash( $workflow_id, $trigger_type, $context );
		if ( isset( self::$active_executions[ $context_hash ] ) ) {
			return array(
				'can_execute' => false,
				'reason'      => 'duplicate_context',
			);
		}

		return array(
			'can_execute' => true,
			'reason'      => null,
		);
	}

	/**
	 * Mark a workflow execution as started.
	 *
	 * Must be called before workflow execution begins.
	 *
	 * @param int    $workflow_id  Workflow ID.
	 * @param array  $context      Trigger context data.
	 * @param string $trigger_type Trigger type identifier.
	 */
	public function begin_execution( int $workflow_id, array $context, string $trigger_type ): void {
		$context_hash = $this->generate_context_hash( $workflow_id, $trigger_type, $context );

		self::$active_executions[ $context_hash ] = true;
	}

	/**
	 * Mark a workflow execution as ended.
	 *
	 * Must be called after workflow execution completes (in finally block).
	 *
	 * @param int    $workflow_id  Workflow ID.
	 * @param array  $context      Trigger context data.
	 * @param string $trigger_type Trigger type identifier.
	 */
	public function end_execution( int $workflow_id, array $context, string $trigger_type ): void {
		$context_hash = $this->generate_context_hash( $workflow_id, $trigger_type, $context );
		unset( self::$active_executions[ $context_hash ] );
	}

	/**
	 * Generate a hash for the execution context.
	 *
	 * Uses workflow_id, trigger_type, and key context fields to create
	 * a unique identifier for this specific trigger event.
	 *
	 * @param int    $workflow_id  Workflow ID.
	 * @param string $trigger_type Trigger type.
	 * @param array  $context      Context data.
	 * @return string MD5 hash of the context.
	 */
	private function generate_context_hash( int $workflow_id, string $trigger_type, array $context ): string {
		// Extract key identifying fields from context.
		// These are the fields that uniquely identify a trigger event.
		$key_fields = $this->extract_key_fields( $context, $trigger_type );

		$hash_data = array(
			'workflow_id'  => $workflow_id,
			'trigger_type' => $trigger_type,
			'key_fields'   => $key_fields,
		);

		return md5( wp_json_encode( $hash_data ) );
	}

	/**
	 * Extract key identifying fields from context based on trigger type.
	 *
	 * Different triggers have different key fields that uniquely identify an event.
	 *
	 * @param array  $context      Context data.
	 * @param string $trigger_type Trigger type.
	 * @return array Key fields for hashing.
	 */
	private function extract_key_fields( array $context, string $trigger_type ): array {
		// Define key fields for each trigger type.
		$trigger_key_fields = array(
			'wordpress/post_published'      => array( 'post_id' ),
			'wordpress/user_registration'   => array( 'user_id' ),
			'wordpress/user_profile_update' => array( 'user_id' ),
			'wordpress/user_role_change'    => array( 'user_id', 'new_role' ),
			'wordpress/comment_submitted'   => array( 'comment_id' ),
		);

		// Get key fields for this trigger type, or use all fields if unknown.
		$fields = $trigger_key_fields[ $trigger_type ] ?? array_keys( $context );

		$key_data = array();
		foreach ( $fields as $field ) {
			if ( isset( $context[ $field ] ) ) {
				$key_data[ $field ] = $context[ $field ];
			}
		}

		return $key_data;
	}

	/**
	 * Mark that an action is being executed.
	 *
	 * Called by ActionQueueProcessor before executing an action.
	 * While set, all new trigger attempts will be blocked.
	 */
	public function begin_action_execution(): void {
		self::$action_executing = true;
	}

	/**
	 * Mark that action execution has ended.
	 *
	 * Called by ActionQueueProcessor after action completes.
	 */
	public function end_action_execution(): void {
		self::$action_executing = false;
	}

	/**
	 * Reset all tracking state.
	 *
	 * Used for testing purposes only.
	 */
	public function reset(): void {
		self::$active_executions = array();
		self::$action_executing  = false;
	}
}
