<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO\JsApi;

use Drupal\flowdrop\Constants\EdgeKeys;

/**
 * A workflow edge exactly as it arrives from (and returns to) the JS editor.
 *
 * This is the *transport* shape: React Flow's camelCase edge record
 * `{id, source, target, sourceHandle, targetHandle, data}`. It is the sole
 * place camelCase JS edge keys are read or written — every other layer should
 * consume this object's typed getters or the domain {@see
 * \Drupal\flowdrop_workflow\DTO\WorkflowEdgeDTO} built from it, never the raw
 * array.
 *
 * Naming the provenance ("this came from the JS API") is the point: the
 * transport edge is a different domain from the internal execution record
 * (snake_case job metadata), even though both spell out the same connection.
 * Keeping them as distinct types removes the dual-case duck-typing that the
 * generic classifier used to need.
 *
 * @internal
 *   Boundary normalisation, not a published contract. The camelCase key set it
 *   owns is deliberately changeable in one place; external code should consume
 *   the domain WorkflowEdgeDTO, not bind to this transport shape.
 */
final class Edge {

  /**
   * Constructs an Edge.
   *
   * @param string $id
   *   The edge ID ('' when the wire omitted it).
   * @param string $source
   *   The source node ID.
   * @param string $target
   *   The target node ID.
   * @param string $sourceHandle
   *   The source handle ("{nodeId}-output-{portName}").
   * @param string $targetHandle
   *   The target handle ("{nodeId}-input-{portName}").
   * @param array<string, mixed> $data
   *   Additional edge data from the editor.
   */
  public function __construct(
    private readonly string $id,
    private readonly string $source,
    private readonly string $target,
    private readonly string $sourceHandle,
    private readonly string $targetHandle,
    private readonly array $data,
  ) {}

  /**
   * Reads a camelCase transport edge array into an Edge.
   *
   * @param array<string, mixed> $edge
   *   The raw edge record from the editor JSON.
   *
   * @return self
   *   The typed transport edge.
   */
  public static function fromArray(array $edge): self {
    return new self(
      id: (string) ($edge['id'] ?? ''),
      source: (string) ($edge[EdgeKeys::SOURCE] ?? ''),
      target: (string) ($edge[EdgeKeys::TARGET] ?? ''),
      sourceHandle: (string) ($edge[EdgeKeys::SOURCE_HANDLE] ?? ''),
      targetHandle: (string) ($edge[EdgeKeys::TARGET_HANDLE] ?? ''),
      data: is_array($edge['data'] ?? NULL) ? $edge['data'] : [],
    );
  }

  /**
   * The edge ID, or '' when the wire omitted it.
   */
  public function getId(): string {
    return $this->id;
  }

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

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

  /**
   * The source handle ("{nodeId}-output-{portName}").
   */
  public function getSourceHandle(): string {
    return $this->sourceHandle;
  }

  /**
   * The target handle ("{nodeId}-input-{portName}").
   */
  public function getTargetHandle(): string {
    return $this->targetHandle;
  }

  /**
   * Additional edge data from the editor.
   *
   * @return array<string, mixed>
   *   The edge data.
   */
  public function getData(): array {
    return $this->data;
  }

  /**
   * Serialises back to the camelCase transport array for egress to the editor.
   *
   * @return array<string, mixed>
   *   The camelCase edge record.
   */
  public function toArray(): array {
    return [
      'id' => $this->id,
      EdgeKeys::SOURCE => $this->source,
      EdgeKeys::TARGET => $this->target,
      EdgeKeys::SOURCE_HANDLE => $this->sourceHandle,
      EdgeKeys::TARGET_HANDLE => $this->targetHandle,
      'data' => $this->data,
    ];
  }

}
