<?php

declare(strict_types=1);

namespace Drupal\flowdrop_stategraph\DTO;

/**
 * Represents a partial state update from node execution.
 *
 * StateUpdate is used to communicate changes from node processors
 * back to the StateManager. Each key in the update data corresponds
 * to a field in the GraphState, and the value will be processed
 * through the appropriate reducer.
 *
 * Example usage:
 * @code
 * // From a node processor returning state updates.
 * return StateUpdate::create([
 *   "messages" => [new Message("assistant", "Response text")],
 *   "data" => ["result" => $processedData],
 * ]);
 *
 * // Creating with a specific source node.
 * return StateUpdate::create([
 *   "currentNodeId" => "process_data",
 * ])->withSourceNode("node_123");
 * @endcode
 *
 * @api
 */
final class StateUpdate implements \JsonSerializable {

  /**
   * Constructs a new StateUpdate.
   *
   * @param array<string, mixed> $data
   *   The partial state update data.
   * @param string|null $sourceNodeId
   *   The ID of the node that produced this update.
   * @param int|null $timestamp
   *   Unix timestamp when the update was created.
   */
  public function __construct(
    private readonly array $data,
    private readonly ?string $sourceNodeId = NULL,
    private readonly ?int $timestamp = NULL,
  ) {}

  /**
   * Factory method to create a StateUpdate from data.
   *
   * @param array<string, mixed> $data
   *   The partial state update data.
   *
   * @return self
   *   A new StateUpdate instance.
   */
  public static function create(array $data): self {
    return new self(
      data: $data,
      timestamp: time(),
    );
  }

  /**
   * Creates a new StateUpdate with source node information.
   *
   * @param string $nodeId
   *   The source node ID.
   *
   * @return self
   *   A new StateUpdate with source node set.
   */
  public function withSourceNode(string $nodeId): self {
    return new self(
      data: $this->data,
      sourceNodeId: $nodeId,
      timestamp: $this->timestamp,
    );
  }

  /**
   * Gets the update data.
   *
   * @return array<string, mixed>
   *   The partial state update data.
   */
  public function getData(): array {
    return $this->data;
  }

  /**
   * Gets a specific value from the update data.
   *
   * @param string $key
   *   The key to retrieve.
   * @param mixed $default
   *   Default value if key doesn't exist.
   *
   * @return mixed
   *   The value or default.
   */
  public function get(string $key, mixed $default = NULL): mixed {
    return $this->data[$key] ?? $default;
  }

  /**
   * Checks if a key exists in the update data.
   *
   * @param string $key
   *   The key to check.
   *
   * @return bool
   *   TRUE if the key exists.
   */
  public function has(string $key): bool {
    return array_key_exists($key, $this->data);
  }

  /**
   * Gets the source node ID.
   *
   * @return string|null
   *   The source node ID or NULL.
   */
  public function getSourceNodeId(): ?string {
    return $this->sourceNodeId;
  }

  /**
   * Gets the timestamp.
   *
   * @return int|null
   *   The unix timestamp or NULL.
   */
  public function getTimestamp(): ?int {
    return $this->timestamp;
  }

  /**
   * Merges this update with another.
   *
   * @param \Drupal\flowdrop_stategraph\DTO\StateUpdate $other
   *   The other update to merge.
   *
   * @return self
   *   A new StateUpdate with merged data.
   */
  public function merge(StateUpdate $other): self {
    return new self(
      data: array_merge($this->data, $other->data),
      sourceNodeId: $other->sourceNodeId ?? $this->sourceNodeId,
      timestamp: time(),
    );
  }

  /**
   * Creates a StateUpdate from an array.
   *
   * Supports two formats:
   * 1. Wrapped format with metadata:
   *    {"data": {...}, "sourceNodeId": "...", "timestamp": ...}
   * 2. Flat format where the entire array is the state data.
   *
   * The method detects wrapped format by checking for "data" key alongside
   * metadata keys ("sourceNodeId" or "timestamp").
   *
   * @param array<string, mixed> $array
   *   The array data.
   *
   * @return self
   *   A new StateUpdate instance.
   */
  public static function fromArray(array $array): self {
    // Detect if this is wrapped format (has "data" key with metadata keys).
    $isWrapped = isset($array["data"])
      && (array_key_exists("sourceNodeId", $array) || array_key_exists("timestamp", $array));

    return new self(
      data: $isWrapped ? $array["data"] : $array,
      sourceNodeId: $array["sourceNodeId"] ?? NULL,
      timestamp: $array["timestamp"] ?? time(),
    );
  }

  /**
   * Converts the update to an array.
   *
   * @return array<string, mixed>
   *   The update as an array.
   */
  public function toArray(): array {
    return [
      "data" => $this->data,
      "sourceNodeId" => $this->sourceNodeId,
      "timestamp" => $this->timestamp,
    ];
  }

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

}
