<?php

declare(strict_types=1);

namespace Drupal\flowdrop_orchestration\DTO\Snapshot;

/**
 * Unified workflow state representation for all orchestrators.
 *
 * WorkflowSnapshot is the central state representation that captures the
 * complete execution state of a workflow at any point in time. It replaces
 * the stategraph-specific GraphState DTO, providing a unified model that
 * all orchestrators can use.
 *
 * Key capabilities:
 * - Track status and output of every node in the workflow.
 * - Support orchestrator handover (sync-to-async) with injected states.
 * - Enable workflow resume from checkpoints.
 * - Store stategraph-specific state (messages, iterator) when needed.
 * - Provide validation of workflow integrity.
 *
 * This DTO is immutable - all modifications return new instances.
 *
 * Example usage:
 * @code
 * // Create a new workflow snapshot
 * $snapshot = WorkflowSnapshot::create(
 *   workflowId: "my_workflow",
 *   workflowVersion: "abc123",
 *   initialInput: ["trigger_data" => $data],
 * );
 *
 * // Add a completed node
 * $snapshot = $snapshot->withNodeCompleted(
 *   nodeId: "node_1",
 *   output: ["result" => "value"],
 * );
 *
 * // Mark nodes as injected (for handover)
 * $snapshot = $snapshot->withInjectedNodes([
 *   "node_1" => ["output" => "data"],
 *   "node_2" => ["output" => "data"],
 * ]);
 * @endcode
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see \Drupal\flowdrop_orchestration\DTO\Snapshot\NodeSnapshot
 * @see \Drupal\flowdrop_runtime\Service\WorkflowState\WorkflowStateManagerInterface
 */
final class WorkflowSnapshot implements \JsonSerializable {

  /**
   * Workflow status constants.
   */
  public const STATUS_PENDING = "pending";
  public const STATUS_RUNNING = "running";
  public const STATUS_COMPLETED = "completed";
  public const STATUS_FAILED = "failed";
  public const STATUS_PAUSED = "paused";
  public const STATUS_CANCELLED = "cancelled";
  public const STATUS_INTERRUPTED = "interrupted";

  /**
   * Valid workflow status values.
   *
   * @var array<string>
   */
  private const VALID_STATUSES = [
    self::STATUS_PENDING,
    self::STATUS_RUNNING,
    self::STATUS_COMPLETED,
    self::STATUS_FAILED,
    self::STATUS_PAUSED,
    self::STATUS_CANCELLED,
    self::STATUS_INTERRUPTED,
  ];

  /**
   * Constructs a new WorkflowSnapshot.
   *
   * @param string $workflowId
   *   The workflow definition ID.
   * @param string $workflowVersion
   *   Hash/version of the workflow definition for validation.
   * @param string $executionId
   *   Unique identifier for this execution instance.
   * @param string $status
   *   Overall workflow status (pending|running|completed|failed|paused).
   * @param array<string, NodeSnapshot> $nodeStates
   *   Map of node ID to NodeSnapshot for each workflow node.
   * @param array<string, mixed> $initialInput
   *   The original input data provided when workflow started.
   * @param array<array<string, mixed>> $messages
   *   Message history for chat/agent workflows (stategraph feature).
   * @param array<string, mixed>|null $iterator
   *   Iterator state for loop processing (stategraph feature).
   * @param int $iterationCount
   *   Current iteration count for loop protection.
   * @param string $threadId
   *   Thread ID for conversation continuity and checkpoint grouping.
   * @param string|null $error
   *   Overall error message if workflow failed.
   * @param array<string, mixed> $metadata
   *   Additional execution metadata.
   * @param int $createdAt
   *   Unix timestamp when snapshot was created.
   * @param int $updatedAt
   *   Unix timestamp when snapshot was last updated.
   */
  public function __construct(
    public readonly string $workflowId,
    public readonly string $workflowVersion,
    public readonly string $executionId,
    public readonly string $status,
    public readonly array $nodeStates,
    public readonly array $initialInput,
    public readonly array $messages = [],
    public readonly ?array $iterator = NULL,
    public readonly int $iterationCount = 0,
    public readonly string $threadId = "",
    public readonly ?string $error = NULL,
    public readonly array $metadata = [],
    public readonly int $createdAt = 0,
    public readonly int $updatedAt = 0,
  ) {
    // Validate status.
    if (!in_array($status, self::VALID_STATUSES, TRUE)) {
      throw new \InvalidArgumentException(
        "Invalid workflow status '{$status}'. Must be one of: " . implode(", ", self::VALID_STATUSES)
      );
    }

    // Validate nodeStates array contains only NodeSnapshot instances.
    foreach ($nodeStates as $nodeId => $nodeSnapshot) {
      if (!$nodeSnapshot instanceof NodeSnapshot) {
        throw new \InvalidArgumentException(
          "nodeStates['{$nodeId}'] must be a NodeSnapshot instance"
        );
      }
    }
  }

  /**
   * Creates a new workflow snapshot with generated IDs.
   *
   * @param string $workflowId
   *   The workflow definition ID.
   * @param string $workflowVersion
   *   The workflow version hash.
   * @param array<string, mixed> $initialInput
   *   The initial input data.
   * @param string|null $threadId
   *   Optional thread ID (generated if not provided).
   * @param array<string, mixed> $metadata
   *   Optional metadata.
   *
   * @return self
   *   A new WorkflowSnapshot.
   */
  public static function create(
    string $workflowId,
    string $workflowVersion,
    array $initialInput = [],
    ?string $threadId = NULL,
    array $metadata = [],
  ): self {
    $now = time();
    return new self(
      workflowId: $workflowId,
      workflowVersion: $workflowVersion,
      executionId: self::generateExecutionId(),
      status: self::STATUS_PENDING,
      nodeStates: [],
      initialInput: $initialInput,
      threadId: $threadId ?? self::generateThreadId(),
      metadata: $metadata,
      createdAt: $now,
      updatedAt: $now,
    );
  }

  /**
   * Creates a workflow snapshot from an existing snapshot (clone with new ID).
   *
   * Useful for creating a new execution based on a previous state.
   *
   * @param self $source
   *   The source snapshot to clone.
   * @param bool $preserveNodeStates
   *   Whether to preserve node states or start fresh.
   *
   * @return self
   *   A new WorkflowSnapshot based on the source.
   */
  public static function createFromSnapshot(
    self $source,
    bool $preserveNodeStates = TRUE,
  ): self {
    $now = time();
    return new self(
      workflowId: $source->workflowId,
      workflowVersion: $source->workflowVersion,
      executionId: self::generateExecutionId(),
      status: self::STATUS_PENDING,
      nodeStates: $preserveNodeStates ? $source->nodeStates : [],
      initialInput: $source->initialInput,
      messages: $preserveNodeStates ? $source->messages : [],
      iterator: $preserveNodeStates ? $source->iterator : NULL,
      iterationCount: 0,
      threadId: $source->threadId,
      metadata: array_merge($source->metadata, [
        "cloned_from" => $source->executionId,
        "cloned_at" => $now,
      ]),
      createdAt: $now,
      updatedAt: $now,
    );
  }

  /**
   * Initializes node states from a workflow definition.
   *
   * Creates pending NodeSnapshot for each node in the workflow.
   *
   * @param array<array<string, mixed>> $workflowNodes
   *   Array of workflow node definitions (must have "id" key).
   *
   * @return self
   *   A new snapshot with initialized node states.
   */
  public function withInitializedNodes(array $workflowNodes): self {
    $nodeStates = $this->nodeStates;

    foreach ($workflowNodes as $node) {
      $nodeId = $node["id"] ?? "";
      if ($nodeId !== "" && !isset($nodeStates[$nodeId])) {
        $nodeStates[$nodeId] = NodeSnapshot::pending($nodeId, [
          "node_type" => $node["data"]["type"] ?? "unknown",
          "label" => $node["data"]["label"] ?? $nodeId,
        ]);
      }
    }

    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $this->status,
      nodeStates: $nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $this->error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Updates the workflow status.
   *
   * @param string $status
   *   The new status.
   *
   * @return self
   *   A new snapshot with updated status.
   */
  public function withStatus(string $status): self {
    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $status,
      nodeStates: $this->nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $this->error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Marks the workflow as running.
   *
   * @return self
   *   A new snapshot with running status.
   */
  public function withRunning(): self {
    return $this->withStatus(self::STATUS_RUNNING);
  }

  /**
   * Marks the workflow as completed.
   *
   * @return self
   *   A new snapshot with completed status.
   */
  public function withCompleted(): self {
    return $this->withStatus(self::STATUS_COMPLETED);
  }

  /**
   * Marks the workflow as paused.
   *
   * @return self
   *   A new snapshot with paused status.
   */
  public function withPaused(): self {
    return $this->withStatus(self::STATUS_PAUSED);
  }

  /**
   * Marks the workflow as failed with an error message.
   *
   * @param string $error
   *   The error message.
   *
   * @return self
   *   A new snapshot with failed status.
   */
  public function withFailed(string $error): self {
    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: self::STATUS_FAILED,
      nodeStates: $this->nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Updates or adds a node state.
   *
   * @param NodeSnapshot $nodeSnapshot
   *   The node snapshot to set.
   *
   * @return self
   *   A new snapshot with updated node state.
   */
  public function withNodeState(NodeSnapshot $nodeSnapshot): self {
    $nodeStates = $this->nodeStates;
    $nodeStates[$nodeSnapshot->nodeId] = $nodeSnapshot;

    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $this->status,
      nodeStates: $nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $this->error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Marks a node as running.
   *
   * @param string $nodeId
   *   The node ID.
   * @param int $executionOrder
   *   The execution order.
   *
   * @return self
   *   A new snapshot with node marked as running.
   */
  public function withNodeRunning(string $nodeId, int $executionOrder = 0): self {
    return $this->withNodeState(
      NodeSnapshot::running($nodeId, $executionOrder)
    );
  }

  /**
   * Marks a node as completed with output.
   *
   * @param string $nodeId
   *   The node ID.
   * @param array<string, mixed> $output
   *   The node output.
   * @param int $executionOrder
   *   The execution order.
   * @param bool $injected
   *   Whether this is injected data.
   *
   * @return self
   *   A new snapshot with node completed.
   */
  public function withNodeCompleted(
    string $nodeId,
    array $output,
    int $executionOrder = 0,
    bool $injected = FALSE,
  ): self {
    $nodeSnapshot = $injected
      ? NodeSnapshot::injected($nodeId, $output, $executionOrder)
      : NodeSnapshot::completed($nodeId, $output, $executionOrder);

    return $this->withNodeState($nodeSnapshot);
  }

  /**
   * Marks a node as failed.
   *
   * @param string $nodeId
   *   The node ID.
   * @param string $error
   *   The error message.
   * @param int $executionOrder
   *   The execution order.
   *
   * @return self
   *   A new snapshot with node failed.
   */
  public function withNodeFailed(
    string $nodeId,
    string $error,
    int $executionOrder = 0,
  ): self {
    return $this->withNodeState(
      NodeSnapshot::failed($nodeId, $error, $executionOrder)
    );
  }

  /**
   * Marks a node as skipped.
   *
   * @param string $nodeId
   *   The node ID.
   * @param string $reason
   *   The skip reason.
   *
   * @return self
   *   A new snapshot with node skipped.
   */
  public function withNodeSkipped(string $nodeId, string $reason = ""): self {
    return $this->withNodeState(
      NodeSnapshot::skipped($nodeId, $reason)
    );
  }

  /**
   * Marks multiple nodes as completed with injected data.
   *
   * Used during orchestrator handover to inject pre-computed node outputs.
   *
   * @param array<string, array<string, mixed>> $nodeOutputs
   *   Map of node ID to output data.
   * @param int $startingExecutionOrder
   *   Starting execution order for the injected nodes.
   *
   * @return self
   *   A new snapshot with injected nodes.
   */
  public function withInjectedNodes(
    array $nodeOutputs,
    int $startingExecutionOrder = 1,
  ): self {
    $snapshot = $this;
    $order = $startingExecutionOrder;

    foreach ($nodeOutputs as $nodeId => $output) {
      $snapshot = $snapshot->withNodeCompleted(
        nodeId: $nodeId,
        output: $output,
        executionOrder: $order++,
        injected: TRUE,
      );
    }

    return $snapshot;
  }

  /**
   * Adds a message to the message history (stategraph feature).
   *
   * @param array<string, mixed> $message
   *   The message to add (should have "role" and "content" keys).
   *
   * @return self
   *   A new snapshot with message added.
   */
  public function withMessage(array $message): self {
    $messages = $this->messages;
    $messages[] = $message;

    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $this->status,
      nodeStates: $this->nodeStates,
      initialInput: $this->initialInput,
      messages: $messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $this->error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Updates the iterator state (stategraph feature).
   *
   * @param array<string, mixed>|null $iterator
   *   The iterator state or NULL to clear.
   *
   * @return self
   *   A new snapshot with updated iterator.
   */
  public function withIterator(?array $iterator): self {
    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $this->status,
      nodeStates: $this->nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $this->error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Increments the iteration count.
   *
   * @return self
   *   A new snapshot with incremented iteration count.
   */
  public function withIncrementedIteration(): self {
    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $this->status,
      nodeStates: $this->nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount + 1,
      threadId: $this->threadId,
      error: $this->error,
      metadata: $this->metadata,
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Adds or updates metadata.
   *
   * @param array<string, mixed> $metadata
   *   Metadata to merge.
   *
   * @return self
   *   A new snapshot with merged metadata.
   */
  public function withMetadata(array $metadata): self {
    return new self(
      workflowId: $this->workflowId,
      workflowVersion: $this->workflowVersion,
      executionId: $this->executionId,
      status: $this->status,
      nodeStates: $this->nodeStates,
      initialInput: $this->initialInput,
      messages: $this->messages,
      iterator: $this->iterator,
      iterationCount: $this->iterationCount,
      threadId: $this->threadId,
      error: $this->error,
      metadata: array_merge($this->metadata, $metadata),
      createdAt: $this->createdAt,
      updatedAt: time(),
    );
  }

  /**
   * Gets a node state by ID.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return NodeSnapshot|null
   *   The node snapshot or NULL if not found.
   */
  public function getNodeState(string $nodeId): ?NodeSnapshot {
    return $this->nodeStates[$nodeId] ?? NULL;
  }

  /**
   * Gets all completed nodes.
   *
   * @return array<string, NodeSnapshot>
   *   Map of node ID to NodeSnapshot for completed nodes.
   */
  public function getCompletedNodes(): array {
    return array_filter(
      $this->nodeStates,
      fn(NodeSnapshot $node) => $node->isCompleted()
    );
  }

  /**
   * Gets all pending nodes.
   *
   * @return array<string, NodeSnapshot>
   *   Map of node ID to NodeSnapshot for pending nodes.
   */
  public function getPendingNodes(): array {
    return array_filter(
      $this->nodeStates,
      fn(NodeSnapshot $node) => $node->isPending()
    );
  }

  /**
   * Gets all injected nodes.
   *
   * @return array<string, NodeSnapshot>
   *   Map of node ID to NodeSnapshot for injected nodes.
   */
  public function getInjectedNodes(): array {
    return array_filter(
      $this->nodeStates,
      fn(NodeSnapshot $node) => $node->injected
    );
  }

  /**
   * Gets all failed nodes.
   *
   * @return array<string, NodeSnapshot>
   *   Map of node ID to NodeSnapshot for failed nodes.
   */
  public function getFailedNodes(): array {
    return array_filter(
      $this->nodeStates,
      fn(NodeSnapshot $node) => $node->isFailed()
    );
  }

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

  /**
   * Gets all node outputs as a map.
   *
   * @return array<string, array<string, mixed>>
   *   Map of node ID to output for nodes with output.
   */
  public function getAllNodeOutputs(): array {
    $outputs = [];
    foreach ($this->nodeStates as $nodeId => $node) {
      if ($node->output !== NULL) {
        $outputs[$nodeId] = $node->output;
      }
    }
    return $outputs;
  }

  /**
   * Gets the highest execution order among all nodes.
   *
   * @return int
   *   The maximum execution order.
   */
  public function getMaxExecutionOrder(): int {
    $max = 0;
    foreach ($this->nodeStates as $node) {
      if ($node->executionOrder > $max) {
        $max = $node->executionOrder;
      }
    }
    return $max;
  }

  /**
   * Checks if the workflow is in a terminal state.
   *
   * @return bool
   *   TRUE if completed, failed, or cancelled.
   */
  public function isTerminal(): bool {
    return in_array($this->status, [
      self::STATUS_COMPLETED,
      self::STATUS_FAILED,
      self::STATUS_CANCELLED,
    ], TRUE);
  }

  /**
   * Checks if the workflow is running.
   *
   * @return bool
   *   TRUE if status is running.
   */
  public function isRunning(): bool {
    return $this->status === self::STATUS_RUNNING;
  }

  /**
   * Checks if the workflow is paused.
   *
   * @return bool
   *   TRUE if status is paused.
   */
  public function isPaused(): bool {
    return $this->status === self::STATUS_PAUSED;
  }

  /**
   * Checks if any nodes are injected.
   *
   * @return bool
   *   TRUE if any node has injected=true.
   */
  public function hasInjectedNodes(): bool {
    foreach ($this->nodeStates as $node) {
      if ($node->injected) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Generates a unique execution ID.
   *
   * @return string
   *   A unique execution ID.
   */
  private static function generateExecutionId(): string {
    return "exec_" . time() . "_" . bin2hex(random_bytes(8));
  }

  /**
   * Generates a unique thread ID.
   *
   * @return string
   *   A unique thread ID.
   */
  private static function generateThreadId(): string {
    return "thread_" . bin2hex(random_bytes(8));
  }

  /**
   * Creates a WorkflowSnapshot from an array.
   *
   * @param array<string, mixed> $data
   *   The snapshot data.
   *
   * @return self
   *   A new WorkflowSnapshot instance.
   */
  public static function fromArray(array $data): self {
    // Parse node states.
    $nodeStates = [];
    $rawNodeStates = $data["nodeStates"] ?? $data["node_states"] ?? [];
    foreach ($rawNodeStates as $nodeId => $nodeData) {
      if ($nodeData instanceof NodeSnapshot) {
        $nodeStates[$nodeId] = $nodeData;
      }
      elseif (is_array($nodeData)) {
        // Ensure nodeId is set in the data.
        if (!isset($nodeData["nodeId"]) && !isset($nodeData["node_id"])) {
          $nodeData["nodeId"] = $nodeId;
        }
        $nodeStates[$nodeId] = NodeSnapshot::fromArray($nodeData);
      }
    }

    return new self(
      workflowId: (string) ($data["workflowId"] ?? $data["workflow_id"] ?? ""),
      workflowVersion: (string) ($data["workflowVersion"] ?? $data["workflow_version"] ?? ""),
      executionId: (string) ($data["executionId"] ?? $data["execution_id"] ?? self::generateExecutionId()),
      status: (string) ($data["status"] ?? self::STATUS_PENDING),
      nodeStates: $nodeStates,
      initialInput: $data["initialInput"] ?? $data["initial_input"] ?? [],
      messages: $data["messages"] ?? [],
      iterator: $data["iterator"] ?? NULL,
      iterationCount: (int) ($data["iterationCount"] ?? $data["iteration_count"] ?? 0),
      threadId: (string) ($data["threadId"] ?? $data["thread_id"] ?? self::generateThreadId()),
      error: isset($data["error"]) ? (string) $data["error"] : NULL,
      metadata: $data["metadata"] ?? [],
      createdAt: (int) ($data["createdAt"] ?? $data["created_at"] ?? time()),
      updatedAt: (int) ($data["updatedAt"] ?? $data["updated_at"] ?? time()),
    );
  }

  /**
   * Converts the snapshot to an array.
   *
   * @return array<string, mixed>
   *   The snapshot as an associative array.
   */
  public function toArray(): array {
    $nodeStatesArray = [];
    foreach ($this->nodeStates as $nodeId => $nodeSnapshot) {
      $nodeStatesArray[$nodeId] = $nodeSnapshot->toArray();
    }

    return [
      "workflowId" => $this->workflowId,
      "workflowVersion" => $this->workflowVersion,
      "executionId" => $this->executionId,
      "status" => $this->status,
      "nodeStates" => $nodeStatesArray,
      "initialInput" => $this->initialInput,
      "messages" => $this->messages,
      "iterator" => $this->iterator,
      "iterationCount" => $this->iterationCount,
      "threadId" => $this->threadId,
      "error" => $this->error,
      "metadata" => $this->metadata,
      "createdAt" => $this->createdAt,
      "updatedAt" => $this->updatedAt,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function jsonSerialize(): mixed {
    return $this->toArray();
  }

}
