<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO\JsApi;

/**
 * A workflow node exactly as it arrives from (and returns to) the JS editor.
 *
 * The transport shape is React Flow's
 * `{id, type, position: {x, y}, data: {label, config, metadata}}`, where
 * `type` is the UI component type (e.g. "universalNode"), NOT the executor
 * plugin. This object is the sole reader of those camelCase transport keys;
 * the domain {@see \Drupal\flowdrop_workflow\DTO\WorkflowNodeDTO} built from it
 * owns the normalisation (executor-plugin resolution, config merging).
 *
 * @see \Drupal\flowdrop\DTO\JsApi\Edge
 *
 * @internal
 *   Boundary normalisation, not a published contract. External code should
 *   consume the domain WorkflowNodeDTO, not bind to this transport shape.
 */
final class Node {

  /**
   * Constructs a Node.
   *
   * @param string $id
   *   The node ID.
   * @param string $type
   *   The UI component type ('' when absent).
   * @param array{x: float, y: float}|null $position
   *   The canvas position, or NULL when absent.
   * @param array<string, mixed> $data
   *   The `data` block ({label, config, metadata}).
   */
  public function __construct(
    private readonly string $id,
    private readonly string $type,
    private readonly ?array $position,
    private readonly array $data,
  ) {}

  /**
   * Reads a camelCase transport node array into a Node.
   *
   * @param array<string, mixed> $node
   *   The raw node record from the editor JSON.
   *
   * @return self
   *   The typed transport node.
   */
  public static function fromArray(array $node): self {
    $position = NULL;
    if (isset($node['position']) && is_array($node['position'])) {
      $position = [
        'x' => (float) ($node['position']['x'] ?? 0),
        'y' => (float) ($node['position']['y'] ?? 0),
      ];
    }

    return new self(
      id: (string) ($node['id'] ?? ''),
      type: (string) ($node['type'] ?? ''),
      position: $position,
      data: is_array($node['data'] ?? NULL) ? $node['data'] : [],
    );
  }

  /**
   * The node ID.
   */
  public function getId(): string {
    return $this->id;
  }

  /**
   * The UI component type ('' when absent).
   */
  public function getType(): string {
    return $this->type;
  }

  /**
   * The canvas position, or NULL when absent.
   *
   * @return array{x: float, y: float}|null
   *   The position.
   */
  public function getPosition(): ?array {
    return $this->position;
  }

  /**
   * The full `data` block ({label, config, metadata}).
   *
   * @return array<string, mixed>
   *   The data block.
   */
  public function getData(): array {
    return $this->data;
  }

  /**
   * The display label from `data.label`, or NULL when absent.
   */
  public function getLabel(): ?string {
    $label = $this->data['label'] ?? NULL;
    return $label === NULL ? NULL : (string) $label;
  }

  /**
   * The user config from `data.config`.
   *
   * @return array<string, mixed>
   *   The config block.
   */
  public function getConfig(): array {
    return is_array($this->data['config'] ?? NULL) ? $this->data['config'] : [];
  }

  /**
   * The node metadata from `data.metadata`.
   *
   * @return array<string, mixed>
   *   The metadata block.
   */
  public function getMetadata(): array {
    return is_array($this->data['metadata'] ?? NULL) ? $this->data['metadata'] : [];
  }

  /**
   * Serialises back to the camelCase transport array for egress to the editor.
   *
   * @return array<string, mixed>
   *   The camelCase node record.
   */
  public function toArray(): array {
    return [
      'id' => $this->id,
      'type' => $this->type,
      'position' => $this->position,
      'data' => $this->data,
    ];
  }

}
