<?php

declare(strict_types=1);

namespace Drupal\flowdrop\DTO\NodeMetadata;

/**
 * Value object representing a node port (input or output).
 *
 * Ports are connection points on nodes in the workflow editor.
 * Each port has a type (input/output), data type, and metadata
 * describing how it can be connected to other nodes.
 *
 * @see \Drupal\flowdrop_node_type\Controller\Api\NodesController::transformSchemaToPorts()
 *
 * @api
 */
final class NodePort {

  /**
   * Port type constant for input ports.
   */
  public const TYPE_INPUT = "input";

  /**
   * Port type constant for output ports.
   */
  public const TYPE_OUTPUT = "output";

  /**
   * Constructs a NodePort instance.
   *
   * @param string $id
   *   The port identifier (e.g., "input_value", "output", "trigger").
   * @param string $name
   *   The human-readable port name.
   * @param string $type
   *   The port type: "input" or "output".
   * @param string $dataType
   *   The data type: "string", "number", "boolean", "json", "trigger", etc.
   * @param bool $required
   *   Whether the port connection is required.
   * @param string $description
   *   The port description.
   * @param mixed $defaultValue
   *   The default value for this port (optional).
   * @param bool $exposedByDefault
   *   Whether this port is exposed (a live runtime override point: shown on the
   *   canvas, wireable, and — for connectable inputs — LLM-fillable) when an
   *   instance has set no explicit exposure for it. Defaults to TRUE. Reserved
   *   ports that should ship hidden until an author opts in (e.g. the `error`
   *   output) declare FALSE.
   * @param int $displayOrder
   *   Default sort weight for the port's render position, ascending; ties break
   *   on declaration order (stable). Author-declared ports default to 0;
   *   reserved injected ports (trigger/tool/error) carry a high weight so they
   *   default to the bottom. Cosmetic only — the engine never reads it. The
   *   instance's `ports` config order overrides this when present.
   */
  public function __construct(
    public readonly string $id,
    public readonly string $name,
    public readonly string $type,
    public readonly string $dataType,
    public readonly bool $required = FALSE,
    public readonly string $description = "",
    public readonly mixed $defaultValue = NULL,
    public readonly bool $exposedByDefault = TRUE,
    public readonly int $displayOrder = 0,
  ) {}

  /**
   * Creates an input port instance.
   *
   * @param string $id
   *   The port identifier.
   * @param string $name
   *   The human-readable port name.
   * @param string $dataType
   *   The data type.
   * @param bool $required
   *   Whether the port connection is required.
   * @param string $description
   *   The port description.
   * @param mixed $defaultValue
   *   The default value for this port.
   *
   * @return self
   *   A new input port instance.
   */
  public static function input(
    string $id,
    string $name,
    string $dataType,
    bool $required = FALSE,
    string $description = "",
    mixed $defaultValue = NULL,
  ): self {
    return new self(
      id: $id,
      name: $name,
      type: self::TYPE_INPUT,
      dataType: $dataType,
      required: $required,
      description: $description,
      defaultValue: $defaultValue,
    );
  }

  /**
   * Creates an output port instance.
   *
   * @param string $id
   *   The port identifier.
   * @param string $name
   *   The human-readable port name.
   * @param string $dataType
   *   The data type.
   * @param string $description
   *   The port description.
   * @param bool $exposedByDefault
   *   Whether the port is exposed when no explicit instance exposure is set.
   * @param int $displayOrder
   *   Default sort weight (see the constructor). Defaults to 0.
   *
   * @return self
   *   A new output port instance.
   */
  public static function output(
    string $id,
    string $name,
    string $dataType,
    string $description = "",
    bool $exposedByDefault = TRUE,
    int $displayOrder = 0,
  ): self {
    return new self(
      id: $id,
      name: $name,
      type: self::TYPE_OUTPUT,
      dataType: $dataType,
      required: FALSE,
      description: $description,
      defaultValue: NULL,
      exposedByDefault: $exposedByDefault,
      displayOrder: $displayOrder,
    );
  }

  /**
   * Creates an instance from an array.
   *
   * @param array<string, mixed> $data
   *   The array data containing port properties.
   *   Expected keys: id, name, type, dataType, required, description,
   *   defaultValue.
   *
   * @return self
   *   A new NodePort instance.
   */
  public static function fromArray(array $data): self {
    return new self(
      id: (string) ($data["id"] ?? ""),
      name: (string) ($data["name"] ?? $data["id"] ?? ""),
      type: (string) ($data["type"] ?? self::TYPE_INPUT),
      dataType: (string) ($data["dataType"] ?? "string"),
      required: (bool) ($data["required"] ?? FALSE),
      description: (string) ($data["description"] ?? ""),
      defaultValue: $data["defaultValue"] ?? NULL,
      exposedByDefault: (bool) ($data["exposedByDefault"] ?? TRUE),
      displayOrder: (int) ($data["displayOrder"] ?? 0),
    );
  }

  /**
   * Converts the port to an array suitable for JSON serialization.
   *
   * @return array<string, mixed>
   *   The array representation matching the API response format.
   */
  public function toArray(): array {
    $result = [
      "id" => $this->id,
      "name" => $this->name,
      "type" => $this->type,
      "dataType" => $this->dataType,
      "required" => $this->required,
      "description" => $this->description,
    ];

    // Only include defaultValue if it's set.
    if ($this->defaultValue !== NULL) {
      $result["defaultValue"] = $this->defaultValue;
    }

    // Only include exposedByDefault when it diverges from the TRUE default, so
    // ordinary ports stay lean and the editor treats a missing key as exposed.
    if ($this->exposedByDefault === FALSE) {
      $result["exposedByDefault"] = FALSE;
    }

    // Only include displayOrder when it diverges from 0 (the natural
    // declaration-order weight), so ordinary ports stay lean.
    if ($this->displayOrder !== 0) {
      $result["displayOrder"] = $this->displayOrder;
    }

    return $result;
  }

  /**
   * Checks if this is an input port.
   *
   * @return bool
   *   TRUE if this is an input port.
   */
  public function isInput(): bool {
    return $this->type === self::TYPE_INPUT;
  }

  /**
   * Checks if this is an output port.
   *
   * @return bool
   *   TRUE if this is an output port.
   */
  public function isOutput(): bool {
    return $this->type === self::TYPE_OUTPUT;
  }

  /**
   * Checks if this port has a default value.
   *
   * @return bool
   *   TRUE if a default value is set.
   */
  public function hasDefaultValue(): bool {
    return $this->defaultValue !== NULL;
  }

}
