<?php
/**
 * Workflow Executor Class
 *
 * Core orchestrator for executing individual workflow actions.
 * Refactored to support per-action queueing where each action is its own job.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Engine;

use Exception;
use Sequensy\Exceptions\ActionException;
use Sequensy\Exceptions\IntegrationException;
use Sequensy\Storage\ActivityLogger;
use Sequensy\Actions\ActionRegistry;
use Sequensy\Engine\VariableReplacer;
use Sequensy\ValueObjects\ActionConfig;
use Sequensy\ValueObjects\ActionResult;

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

/**
 * Class WorkflowExecutor
 *
 * Executes individual actions within a workflow with comprehensive
 * error handling, logging, and variable replacement.
 */
class WorkflowExecutor {

	/**
	 * Activity logger instance.
	 *
	 * @var ActivityLogger
	 */
	private ActivityLogger $logger;

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

	/**
	 * Variable replacer instance.
	 *
	 * @var VariableReplacer
	 */
	private VariableReplacer $variable_replacer;

	/**
	 * Constructor.
	 *
	 * @param ActivityLogger|null   $logger            Activity logger instance.
	 * @param ActionRegistry|null   $action_registry   Action registry instance.
	 * @param VariableReplacer|null $variable_replacer Variable replacer instance.
	 */
	public function __construct(
		?ActivityLogger $logger = null,
		?ActionRegistry $action_registry = null,
		?VariableReplacer $variable_replacer = null
	) {
		$this->logger            = $logger ?? new ActivityLogger();
		$this->action_registry   = $action_registry ?? ActionRegistry::get_instance();
		$this->variable_replacer = $variable_replacer ?? new VariableReplacer();
	}

	/**
	 * Execute a single action with the given namespaced context.
	 *
	 * This method is called by the Action Queue Processor for each action job.
	 * It handles variable replacement, action execution, and logging.
	 *
	 * Exceptions thrown by the action handler ({@see ActionException},
	 * {@see IntegrationException}, generic {@see Exception}) are caught
	 * internally and converted to failure result arrays.
	 *
	 * @param int                  $workflow_id   Workflow ID for logging.
	 * @param ActionConfig         $action        Action configuration value object.
	 * @param array<string, mixed> $context       Full namespaced context (Trigger + previous action outputs).
	 * @param int                  $execution_id  Execution ID for logging.
	 * @param string|null          $action_id     Action ID for logging context.
	 * @return array<string, mixed> Result with 'success', 'data', and optionally 'error' and 'retryable'.
	 */
	public function execute_single_action(
		int $workflow_id,
		ActionConfig $action,
		array $context,
		int $execution_id,
		?string $action_id = null
	): array {
		$start_time   = microtime( true );
		$action_type  = $action->get_type();
		$action_label = $action->get_label() ?? $action_type;

		// Resolve action handler from registry.
		// Integration-type packages are deferred at boot. Try loading on-demand.
		$action_handler = $this->action_registry->get_action( $action_type );
		if ( ! $action_handler && str_contains( $action_type, '/' ) ) {
			$integration = explode( '/', $action_type, 2 )[0];
			do_action( 'sequensy_before_execute_action', $action_type, $integration );
			$action_handler = $this->action_registry->get_action( $action_type );
		}
		if ( ! $action_handler ) {
			$this->logger->error(
				$workflow_id,
				"Action handler not found for type: $action_type",
				array(
					'action_id'   => $action_id,
					'action_type' => $action_type,
				),
				null,
				$execution_id
			);

			return array(
				'success' => false,
				'error'   => "Unknown action type: $action_type",
			);
		}

		// Build execution context: replace variables and merge connection_id.
		list( $action_config, $context ) = $this->build_action_context( $action, $context );

		// Log action execution start.
		$this->logger->info(
			$workflow_id,
			"Executing action: $action_label ($action_type)",
			array(
				'action_id'     => $action_id,
				'action_type'   => $action_type,
				'action_label'  => $action_label,
				'action_config' => $action_config,
			),
			null,
			$execution_id
		);

		// Execute and normalize result, catching typed exceptions.
		$result = $this->normalize_action_result(
			$action_handler,
			$action_config,
			$context,
			$workflow_id,
			$action_type,
			$action_id,
			$execution_id
		);

		// Log completion or failure.
		$duration = microtime( true ) - $start_time;
		$this->log_action_result(
			$result,
			$workflow_id,
			$action_label,
			$action_type,
			$action_id,
			$duration,
			$execution_id
		);

		return $result;
	}

	/**
	 * Build the action execution context.
	 *
	 * Performs variable replacement on the action config and merges the
	 * connection_id into the context for multi-account support.
	 *
	 * @param ActionConfig         $action  Action configuration value object.
	 * @param array<string, mixed> $context Full namespaced context.
	 * @return array{0: array<string, mixed>, 1: array<string, mixed>} Tuple of [resolved_config, enriched_context].
	 */
	private function build_action_context( ActionConfig $action, array $context ): array {
		// Replace variables in action config.
		$action_config = $this->variable_replacer->replace( $action->get_config(), $context );

		// Add connection_id to context for multi-account support.
		$connection_id = $action->get_connection_id();
		if ( null !== $connection_id ) {
			$context['_connection_id'] = $connection_id;
		}

		return array( $action_config, $context );
	}

	/**
	 * Execute the action handler and normalize the result.
	 *
	 * Runs the handler, converts ActionResult objects to arrays, and catches
	 * ActionException, IntegrationException, and generic Exception, converting
	 * them into standardized failure result arrays.
	 *
	 * @param \Sequensy\Actions\Contracts\ActionHandler $handler       The action handler to execute.
	 * @param array<string, mixed>                      $action_config Resolved action configuration.
	 * @param array<string, mixed>                      $context       Full execution context.
	 * @param int                                       $workflow_id   Workflow ID for logging.
	 * @param string                                    $action_type   Action type identifier.
	 * @param string|null                               $action_id     Action ID for logging context.
	 * @param int                                       $execution_id  Execution ID for logging.
	 * @return array<string, mixed> Normalized result with 'success', 'data', and optionally 'error' and 'retryable'.
	 */
	private function normalize_action_result(
		$handler,
		array $action_config,
		array $context,
		int $workflow_id,
		string $action_type,
		?string $action_id,
		int $execution_id
	): array {
		try {
			$result = $handler->execute( $action_config, $context );
			return $this->normalize_result( $result );
		} catch ( ActionException $e ) {
			$this->logger->error(
				$workflow_id,
				"Action threw ActionException: $action_type - " . $e->getMessage(),
				array_merge(
					$e->get_context(),
					array(
						'action_id'   => $action_id,
						'action_type' => $action_type,
						'retryable'   => $e->is_retryable(),
						'trace'       => $e->getTraceAsString(),
					)
				),
				null,
				$execution_id
			);

			return array(
				'success'   => false,
				'error'     => $e->getMessage(),
				'retryable' => $e->is_retryable(),
			);
		} catch ( IntegrationException $e ) {
			$this->logger->error(
				$workflow_id,
				"Action threw IntegrationException: $action_type - " . $e->getMessage(),
				array_merge(
					$e->get_context(),
					array(
						'action_id'    => $action_id,
						'action_type'  => $action_type,
						'integration'  => $e->get_integration(),
						'status_code'  => $e->get_status_code(),
						'retryable'    => $e->is_retryable(),
						'rate_limited' => $e->is_rate_limited(),
						'auth_error'   => $e->is_auth_error(),
						'trace'        => $e->getTraceAsString(),
					)
				),
				null,
				$execution_id
			);

			return array(
				'success'   => false,
				'error'     => $e->getMessage(),
				'retryable' => $e->is_retryable(),
			);
		} catch ( Exception $e ) {
			$this->logger->error(
				$workflow_id,
				"Action threw exception: $action_type - " . $e->getMessage(),
				array(
					'action_id'   => $action_id,
					'action_type' => $action_type,
					'exception'   => $e->getMessage(),
					'trace'       => $e->getTraceAsString(),
				),
				null,
				$execution_id
			);

			return array(
				'success'   => false,
				'error'     => $e->getMessage(),
				'retryable' => false,
			);
		}
	}

	/**
	 * Log the action execution result.
	 *
	 * Logs a success or failure message with timing information.
	 *
	 * @param array<string, mixed> $result       Action result array.
	 * @param int                  $workflow_id   Workflow ID for logging.
	 * @param string               $action_label  Human-readable action label.
	 * @param string               $action_type   Action type identifier.
	 * @param string|null          $action_id     Action ID for logging context.
	 * @param float                $duration      Execution duration in seconds.
	 * @param int                  $execution_id  Execution ID for logging.
	 */
	private function log_action_result(
		array $result,
		int $workflow_id,
		string $action_label,
		string $action_type,
		?string $action_id,
		float $duration,
		int $execution_id
	): void {
		if ( isset( $result['success'] ) && $result['success'] ) {
			$this->logger->info(
				$workflow_id,
				"Action completed successfully: $action_label",
				array(
					'action_id'   => $action_id,
					'action_type' => $action_type,
					'duration'    => round( $duration, 3 ),
					'result'      => $result,
				),
				null,
				$execution_id
			);
		} else {
			$this->logger->error(
				$workflow_id,
				"Action failed: $action_label",
				array(
					'action_id'   => $action_id,
					'action_type' => $action_type,
					'duration'    => round( $duration, 3 ),
					'error'       => $result['error'] ?? 'Unknown error',
				),
				null,
				$execution_id
			);
		}
	}

	// phpcs:disable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
	/**
	 * Generate namespace identifier for an action.
	 *
	 * Uses the action's user-defined label if available, otherwise falls back
	 * to generating a default label from the action type.
	 *
	 * @param ActionConfig $action    Action configuration value object.
	 * @param string|null  $action_id Action ID (unused, kept for call-site compatibility).
	 * @return string Namespace like "create_post" or "send_email".
	 */
	public function get_action_namespace(
		ActionConfig $action,
		?string $action_id = null
	): string {
		// phpcs:enable Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed
		// Use user-defined label if provided.
		$label = $action->get_label();
		if ( null !== $label && '' !== $label ) {
			return $this->sanitize_namespace( $label );
		}

		// Fallback: derive from action type.
		$type = $action->get_type();
		// Strip package prefix (e.g., "wordpress/create_post" -> "create_post").
		if ( str_contains( $type, '/' ) ) {
			$type = substr( $type, strrpos( $type, '/' ) + 1 );
		}

		return $this->sanitize_namespace( $type );
	}

	/**
	 * Sanitizes a label to be used as a namespace key.
	 * Converts to snake_case.
	 *
	 * IMPORTANT: This function must stay in sync with the frontend implementation:
	 *
	 * @see admin/src/utils/namespace.ts::sanitizeNamespace()
	 *
	 * Any changes here must be mirrored in the TypeScript version to ensure
	 * backend context keys match frontend variable paths.
	 *
	 * @param string $label The label to sanitize.
	 * @return string Sanitized namespace key.
	 */
	public function sanitize_namespace( string $label ): string {
		// Convert to lowercase
		$result = strtolower( $label );
		// Replace non-alphanumeric characters with underscore
		$result = preg_replace( '/[^a-z0-9]+/', '_', $result );
		// Trim leading/trailing underscores
		$result = trim( $result, '_' );
		return $result;
	}

	/**
	 * Replace template variables in action config.
	 *
	 * Delegates to VariableReplacer. This public method is kept for backward
	 * compatibility with existing callers and tests.
	 *
	 * Supports namespaced {{Namespace.field}} syntax where Namespace is either
	 * 'Trigger' or a user-defined action label (e.g., 'create_post', 'send_email').
	 *
	 * @param mixed                $value   Value to process (string, array, or other).
	 * @param array<string, mixed> $context Namespaced context data for variable replacement.
	 * @return mixed Processed value with variables replaced.
	 */
	public function replace_variables( mixed $value, array $context ): mixed {
		if ( is_array( $value ) ) {
			return $this->variable_replacer->replace( $value, $context );
		}

		// For non-array values, wrap/unwrap to reuse VariableReplacer.
		if ( is_string( $value ) ) {
			$result = $this->variable_replacer->replace( array( '_' => $value ), $context );
			return $result['_'];
		}

		return $value;
	}

	/**
	 * Normalize action result to array format.
	 *
	 * Converts ActionResult objects to arrays for internal processing.
	 * Some actions may return arrays and others return ActionResult.
	 *
	 * @param mixed $result Action result (ActionResult or array).
	 * @return array<string, mixed> Normalized result array.
	 */
	private function normalize_result( mixed $result ): array {
		if ( $result instanceof ActionResult ) {
			return $result->to_array();
		}

		// Already an array, return as-is
		if ( is_array( $result ) ) {
			return $result;
		}

		// Unexpected result type, wrap in error
		return array(
			'success' => false,
			'error'   => 'Action returned unexpected result type: ' . gettype( $result ),
		);
	}
}
