<?php

declare(strict_types=1);

namespace Drupal\flowdrop_orchestration\DTO\Orchestrator;

use Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot;

/**
 * Request to orchestrate a workflow.
 *
 * OrchestrationRequest encapsulates all data needed to start or resume
 * a workflow execution. It supports two modes of operation:
 *
 * 1. **Fresh execution**: Provide workflow definition and initial data.
 * 2. **Resume/Handover**: Provide a WorkflowSnapshot with pre-populated
 *    node states from a previous execution context.
 *
 * The snapshot enables:
 * - Orchestrator handover (sync-to-async) with completed node outputs.
 * - Workflow resume from checkpoints.
 * - Trigger efficiency by injecting pre-computed node outputs.
 *
 * Example usage:
 * @code
 * // Fresh execution
 * $request = new OrchestrationRequest(
 *   workflowId: "my_workflow",
 *   pipelineId: "pipeline_123",
 *   workflow: $workflowDefinition,
 *   initialData: ["trigger_data" => $data],
 *   options: ["orchestrator" => "flowdrop_runtime:asynchronous"],
 * );
 *
 * // Handover with snapshot
 * $request = new OrchestrationRequest(
 *   workflowId: "my_workflow",
 *   pipelineId: "pipeline_456",
 *   workflow: $workflowDefinition,
 *   initialData: [],
 *   options: [],
 *   initialSnapshot: $snapshotFromSyncExecution,
 * );
 * @endcode
 *
 * @see \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot
 *
 * @internal
 *   This class is not part of the public API and may change without notice.
 */
class OrchestrationRequest {

  /**
   * Constructs a new OrchestrationRequest.
   *
   * @param string $workflowId
   *   The workflow definition ID.
   * @param string $pipelineId
   *   The pipeline ID for this execution (used for tracking).
   * @param array<string, mixed> $workflow
   *   The workflow definition array containing nodes and edges.
   * @param array<string, mixed> $initialData
   *   Initial input data for the workflow (trigger data, user input, etc.).
   * @param array<string, mixed> $options
   *   Orchestration options (orchestrator type, max_concurrent_jobs, etc.).
   * @param \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot|null $initialSnapshot
   *   Optional snapshot with pre-populated node states for resume/handover.
   *   When provided, completed nodes in the snapshot will be marked as
   *   "injected" in the new execution context.
   */
  public function __construct(
    private readonly string $workflowId,
    private readonly string $pipelineId,
    private readonly array $workflow,
    private readonly array $initialData,
    private readonly array $options,
    private readonly ?WorkflowSnapshot $initialSnapshot = NULL,
  ) {}

  /**
   * Creates a request for fresh workflow execution.
   *
   * @param string $workflowId
   *   The workflow ID.
   * @param array<string, mixed> $workflow
   *   The workflow definition.
   * @param array<string, mixed> $initialData
   *   The initial input data.
   * @param array<string, mixed> $options
   *   Optional orchestration options.
   *
   * @return self
   *   A new OrchestrationRequest.
   */
  public static function create(
    string $workflowId,
    array $workflow,
    array $initialData = [],
    array $options = [],
  ): self {
    $pipelineId = "pipeline_" . $workflowId . "_" . time();
    return new self(
      workflowId: $workflowId,
      pipelineId: $pipelineId,
      workflow: $workflow,
      initialData: $initialData,
      options: $options,
    );
  }

  /**
   * Creates a request for handover from a snapshot.
   *
   * Use this when handing over execution from one orchestrator to another,
   * preserving the completed node states from the source execution.
   *
   * @param \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot $snapshot
   *   The snapshot containing completed node states.
   * @param array<string, mixed> $workflow
   *   The workflow definition.
   * @param array<string, mixed> $options
   *   Optional orchestration options.
   *
   * @return self
   *   A new OrchestrationRequest with the snapshot.
   */
  public static function fromSnapshot(
    WorkflowSnapshot $snapshot,
    array $workflow,
    array $options = [],
  ): self {
    $pipelineId = "pipeline_" . $snapshot->workflowId . "_" . time();
    return new self(
      workflowId: $snapshot->workflowId,
      pipelineId: $pipelineId,
      workflow: $workflow,
      initialData: $snapshot->initialInput,
      options: $options,
      initialSnapshot: $snapshot,
    );
  }

  /**
   * Get the workflow ID.
   *
   * @return string
   *   The workflow ID.
   */
  public function getWorkflowId(): string {
    return $this->workflowId;
  }

  /**
   * Get the pipeline ID.
   *
   * @return string
   *   The pipeline ID.
   */
  public function getPipelineId(): string {
    return $this->pipelineId;
  }

  /**
   * Get the workflow definition.
   *
   * @return array<string, mixed>
   *   The workflow definition.
   */
  public function getWorkflow(): array {
    return $this->workflow;
  }

  /**
   * Get the initial data.
   *
   * @return array<string, mixed>
   *   The initial data.
   */
  public function getInitialData(): array {
    return $this->initialData;
  }

  /**
   * Get the orchestration options.
   *
   * @return array<string, mixed>
   *   The orchestration options.
   */
  public function getOptions(): array {
    return $this->options;
  }

  /**
   * Get the initial snapshot.
   *
   * @return \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot|null
   *   The snapshot or NULL if not provided.
   */
  public function getInitialSnapshot(): ?WorkflowSnapshot {
    return $this->initialSnapshot;
  }

  /**
   * Checks if this request has a pre-loaded snapshot.
   *
   * When TRUE, the orchestrator should initialize node states from the
   * snapshot rather than starting fresh.
   *
   * @return bool
   *   TRUE if an initial snapshot is provided.
   */
  public function hasPreloadedState(): bool {
    return $this->initialSnapshot !== NULL;
  }

  /**
   * Gets the node outputs from the snapshot for injection.
   *
   * Returns a map of node ID to output for all completed nodes in the
   * snapshot. Use this to inject pre-computed outputs into job entities.
   *
   * @return array<string, array<string, mixed>>
   *   Map of node ID to output data, or empty array if no snapshot.
   */
  public function getPreloadedNodeOutputs(): array {
    if ($this->initialSnapshot === NULL) {
      return [];
    }

    return $this->initialSnapshot->getAllNodeOutputs();
  }

  /**
   * Gets the IDs of completed nodes from the snapshot.
   *
   * @return array<string>
   *   Array of node IDs that are completed in the snapshot.
   */
  public function getPreloadedCompletedNodeIds(): array {
    if ($this->initialSnapshot === NULL) {
      return [];
    }

    return array_keys($this->initialSnapshot->getCompletedNodes());
  }

  /**
   * Checks if a specific node is pre-completed in the snapshot.
   *
   * @param string $nodeId
   *   The node ID to check.
   *
   * @return bool
   *   TRUE if the node is completed in the snapshot.
   */
  public function isNodePreloaded(string $nodeId): bool {
    if ($this->initialSnapshot === NULL) {
      return FALSE;
    }

    $nodeState = $this->initialSnapshot->getNodeState($nodeId);
    return $nodeState !== NULL && $nodeState->isCompleted();
  }

  /**
   * Gets the output of a pre-completed node.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return array<string, mixed>|null
   *   The node output or NULL if not available.
   */
  public function getPreloadedNodeOutput(string $nodeId): ?array {
    if ($this->initialSnapshot === NULL) {
      return NULL;
    }

    return $this->initialSnapshot->getNodeOutput($nodeId);
  }

  /**
   * Gets a specific option value.
   *
   * @param string $key
   *   The option key.
   * @param mixed $default
   *   Default value if option not set.
   *
   * @return mixed
   *   The option value or default.
   */
  public function getOption(string $key, mixed $default = NULL): mixed {
    return $this->options[$key] ?? $default;
  }

  /**
   * Creates a new request with updated options.
   *
   * @param array<string, mixed> $options
   *   Options to merge.
   *
   * @return self
   *   A new request with merged options.
   */
  public function withOptions(array $options): self {
    return new self(
      workflowId: $this->workflowId,
      pipelineId: $this->pipelineId,
      workflow: $this->workflow,
      initialData: $this->initialData,
      options: array_merge($this->options, $options),
      initialSnapshot: $this->initialSnapshot,
    );
  }

  /**
   * Creates a new request with a snapshot attached.
   *
   * @param \Drupal\flowdrop_orchestration\DTO\Snapshot\WorkflowSnapshot $snapshot
   *   The snapshot to attach.
   *
   * @return self
   *   A new request with the snapshot.
   */
  public function withSnapshot(WorkflowSnapshot $snapshot): self {
    return new self(
      workflowId: $this->workflowId,
      pipelineId: $this->pipelineId,
      workflow: $this->workflow,
      initialData: $this->initialData,
      options: $this->options,
      initialSnapshot: $snapshot,
    );
  }

  /**
   * Convert to array for serialization.
   *
   * @return array<string, mixed>
   *   Array representation of the request.
   */
  public function toArray(): array {
    $data = [
      "workflow_id" => $this->workflowId,
      "pipeline_id" => $this->pipelineId,
      "workflow" => $this->workflow,
      "initial_data" => $this->initialData,
      "options" => $this->options,
    ];

    if ($this->initialSnapshot !== NULL) {
      $data["initial_snapshot"] = $this->initialSnapshot->toArray();
    }

    return $data;
  }

  /**
   * Creates an OrchestrationRequest from an array.
   *
   * @param array<string, mixed> $data
   *   The request data.
   *
   * @return self
   *   A new OrchestrationRequest.
   */
  public static function fromArray(array $data): self {
    $snapshot = NULL;
    if (isset($data["initial_snapshot"]) && is_array($data["initial_snapshot"])) {
      $snapshot = WorkflowSnapshot::fromArray($data["initial_snapshot"]);
    }

    return new self(
      workflowId: (string) ($data["workflow_id"] ?? ""),
      pipelineId: (string) ($data["pipeline_id"] ?? ""),
      workflow: $data["workflow"] ?? [],
      initialData: $data["initial_data"] ?? [],
      options: $data["options"] ?? [],
      initialSnapshot: $snapshot,
    );
  }

}
