<?php
/**
 * Queue Manager Class
 *
 * Facade for Action Scheduler operations, managing workflow execution queue.
 *
 * @package Sequensy
 * @since 2.0.0
 */

declare(strict_types=1);

namespace Sequensy\Engine;

use Exception;
use Sequensy\Traits\ErrorHandler;
use Sequensy\Storage\ActivityLogger;
use Sequensy\Storage\ExecutionStorage;
use Sequensy\Storage\WorkflowStorage;

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

/**
 * Class QueueManager
 *
 * Provides abstraction layer for Action Scheduler queue operations.
 * Action Scheduler is always available (bundled with plugin).
 */
class QueueManager {

	use ErrorHandler;

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

	/**
	 * Execution storage instance.
	 *
	 * @var ExecutionStorage
	 */
	private ExecutionStorage $execution_storage;

	/**
	 * Workflow storage instance.
	 *
	 * @var WorkflowStorage
	 */
	private WorkflowStorage $workflow_storage;

	/**
	 * Context serializer instance.
	 *
	 * @var ContextSerializer
	 */
	private ContextSerializer $context_serializer;

	/**
	 * Constructor.
	 *
	 * @param ActivityLogger|null    $logger              Optional activity logger instance.
	 * @param ExecutionStorage|null  $execution_storage   Optional execution storage instance.
	 * @param WorkflowStorage|null   $workflow_storage    Optional workflow storage instance.
	 * @param ContextSerializer|null $context_serializer  Optional context serializer instance.
	 */
	public function __construct(
		?ActivityLogger $logger = null,
		?ExecutionStorage $execution_storage = null,
		?WorkflowStorage $workflow_storage = null,
		?ContextSerializer $context_serializer = null
	) {
		$this->logger             = $logger ?? new ActivityLogger();
		$this->execution_storage  = $execution_storage ?? new ExecutionStorage();
		$this->workflow_storage   = $workflow_storage ?? new WorkflowStorage();
		$this->context_serializer = $context_serializer ?? new ContextSerializer();
	}

	/**
	 * Start a new workflow execution.
	 *
	 * Creates an execution record and enqueues the first action.
	 * This is the main entry point for per-action queueing.
	 *
	 * @param int                  $workflow_id Workflow ID.
	 * @param array<string, mixed> $context     Trigger context data.
	 * @return int|false Execution ID on success, false on failure.
	 *
	 * @throws \Sequensy\Exceptions\StorageException When the workflow record cannot be loaded from the database.
	 */
	public function start_execution( int $workflow_id, array $context = array() ): int|false {
		// Step 1: Validate context is serializable.
		if ( ! $this->prepare_context( $context, $workflow_id ) ) {
			return false;
		}

		// Step 2: Load and validate published workflow config.
		$published = $this->resolve_published_config( $workflow_id );
		if ( ! $published ) {
			return false;
		}

		// Step 3: Resolve entry action and create execution record.
		$entry_action_id = $published['config']->get_entry_action_id();
		if ( ! $entry_action_id ) {
			$this->log_error(
				'No entry action found in workflow config',
				array( 'workflow_id' => $workflow_id ),
				$workflow_id
			);
			return false;
		}

		$execution_id = $this->create_execution_record(
			$workflow_id,
			$context,
			$published['config'],
			$published['version'],
			$entry_action_id
		);
		if ( ! $execution_id ) {
			return false;
		}

		// Step 4: Enqueue the first action for async processing.
		$job_id = $this->enqueue_action( $execution_id, $entry_action_id );

		if ( ! $job_id ) {
			$this->log_error(
				'Failed to enqueue first action',
				array(
					'workflow_id'  => $workflow_id,
					'execution_id' => $execution_id,
				),
				$workflow_id
			);
			$this->execution_storage->fail_execution( $execution_id, 'Failed to enqueue first action' );
			return false;
		}

		// Log execution started.
		$this->logger->info(
			$workflow_id,
			'Workflow execution started',
			array(
				'execution_id'   => $execution_id,
				'version_id'     => (int) $published['version']->id,
				'version_number' => (int) $published['version']->version_number,
				'total_actions'  => $published['config']->get_total_actions(),
				'job_id'         => $job_id,
				'trigger_type'   => $published['workflow']->trigger_type,
			),
			null,
			$execution_id
		);

		return $execution_id;
	}

	/**
	 * Validate and prepare execution context.
	 *
	 * Uses the ContextSerializer to verify the context is serializable
	 * and within size limits. Logs errors and warnings as needed.
	 *
	 * @param array<string, mixed> $context     Trigger context data.
	 * @param int                  $workflow_id Workflow ID for error logging.
	 * @return bool True if context is valid, false otherwise.
	 */
	private function prepare_context( array $context, int $workflow_id ): bool {
		$validation_result = $this->context_serializer->validate( $context );

		if ( ! $validation_result->is_valid() ) {
			$this->log_error(
				'Context validation failed: ' . $validation_result->get_error(),
				array( 'workflow_id' => $workflow_id ),
				$workflow_id
			);
			return false;
		}

		if ( $validation_result->has_warnings() ) {
			$this->log_warning( $validation_result->get_warning() );
		}

		return true;
	}

	/**
	 * Resolve the published workflow configuration and version info.
	 *
	 * Loads the workflow, verifies it has been published, fetches the published
	 * config, and retrieves the version record from the database.
	 *
	 * @param int $workflow_id Workflow ID.
	 * @return array{workflow: object, config: \Sequensy\ValueObjects\WorkflowConfig, version: object}|false
	 *     Associative array with workflow, config, and version on success, false on failure.
	 */
	private function resolve_published_config( int $workflow_id ): array|false {
		// Load workflow to get published version info.
		$workflow = $this->workflow_storage->get_workflow( $workflow_id );
		if ( ! $workflow ) {
			$this->log_error(
				'Workflow not found',
				array( 'workflow_id' => $workflow_id ),
				$workflow_id
			);
			return false;
		}

		// Verify workflow has been published.
		if ( ! $workflow->published_version_id ) {
			$this->log_error(
				'Workflow has no published version',
				array( 'workflow_id' => $workflow_id ),
				$workflow_id
			);
			return false;
		}

		// Load the published config (not draft).
		$config = $this->workflow_storage->get_published_config( $workflow_id );
		if ( ! $config ) {
			$this->log_error(
				'Failed to load published config',
				array( 'workflow_id' => $workflow_id ),
				$workflow_id
			);
			return false;
		}

		$actions = $config->get_actions();
		if ( empty( $actions ) ) {
			$this->logger->warning( $workflow_id, 'No actions configured in published version' );
			return false;
		}

		// Get version info for execution record.
		global $wpdb;
		// phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared
		$version = $wpdb->get_row(
			$wpdb->prepare(
				"SELECT id, version_number FROM {$wpdb->prefix}sequensy_workflow_versions WHERE id = %d",
				$workflow->published_version_id
			)
		);

		if ( ! $version ) {
			$this->log_error(
				'Published version record not found',
				array(
					'workflow_id' => $workflow_id,
					'version_id'  => $workflow->published_version_id,
				),
				$workflow_id
			);
			return false;
		}

		return array(
			'workflow' => $workflow,
			'config'   => $config,
			'version'  => $version,
		);
	}

	/**
	 * Create the execution record in the database.
	 *
	 * Builds the namespaced context (with Trigger data and meta for loop prevention)
	 * and delegates to ExecutionStorage for the database insert.
	 *
	 * @param int                                   $workflow_id     Workflow ID.
	 * @param array<string, mixed>                  $context         Raw trigger context data.
	 * @param \Sequensy\ValueObjects\WorkflowConfig $config          Published workflow config.
	 * @param object                                $version         Version record with id and version_number.
	 * @param string                                $entry_action_id Entry action ID for the execution.
	 * @return int|false Execution ID on success, false on failure.
	 */
	private function create_execution_record(
		int $workflow_id,
		array $context,
		$config,
		object $version,
		string $entry_action_id
	): int|false {
		$total_actions = $config->get_total_actions();

		// Build namespaced context with loop prevention metadata.
		$namespaced_context = array(
			'Trigger'        => $context,
			'_sequensy_meta' => array(
				'workflow_id' => $workflow_id,
				'depth'       => 1,
			),
		);

		$execution_id = $this->execution_storage->create_execution(
			$workflow_id,
			$namespaced_context,
			$total_actions,
			(int) $version->id,
			(int) $version->version_number,
			$entry_action_id
		);

		if ( ! $execution_id ) {
			$this->log_error(
				'Failed to create execution record',
				array( 'workflow_id' => $workflow_id ),
				$workflow_id
			);
			return false;
		}

		return $execution_id;
	}

	/**
	 * Enqueue a specific action for execution.
	 *
	 * @param int    $execution_id Execution ID.
	 * @param string $action_id    Action ID.
	 * @return int|false Job ID on success, false on failure.
	 */
	public function enqueue_action( int $execution_id, string $action_id ): int|false {
		try {
			$job_id = as_enqueue_async_action(
				'sequensy_execute_action',
				array(
					'execution_id' => $execution_id,
					'action_id'    => $action_id,
				),
				'sequensy-workflows'
			);

			return $job_id;
		} catch ( Exception $e ) {
			$this->log_error(
				'Failed to enqueue action: ' . $e->getMessage(),
				array(
					'execution_id' => $execution_id,
					'action_id'    => $action_id,
				)
			);
			return false;
		}
	}

	/**
	 * Get pending action jobs.
	 *
	 * Returns Action Scheduler job IDs for pending action executions.
	 *
	 * @return array<int> Array of job IDs.
	 */
	public function get_pending_jobs(): array {
		$args = array(
			'hook'   => 'sequensy_execute_action',
			'status' => 'pending',
			'group'  => 'sequensy-workflows',
		);

		try {
			$actions = as_get_scheduled_actions( $args, 'ids' );
			return $actions;
		} catch ( Exception $e ) {
			$this->log_error( 'Error fetching pending jobs: ' . $e->getMessage() );
			return array();
		}
	}

	/**
	 * Get failed action jobs.
	 *
	 * Returns Action Scheduler job IDs for failed action executions.
	 *
	 * @return array<int> Array of job IDs.
	 */
	public function get_failed_jobs(): array {
		$args = array(
			'hook'   => 'sequensy_execute_action',
			'status' => 'failed',
			'group'  => 'sequensy-workflows',
		);

		try {
			$actions = as_get_scheduled_actions( $args, 'ids' );
			return $actions;
		} catch ( Exception $e ) {
			$this->log_error( 'Error fetching failed jobs: ' . $e->getMessage() );
			return array();
		}
	}
}
