<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\DTO\Compiler;

/**
 * Represents a node excluded from the execution graph.
 *
 * Tracks why a node was excluded from execution, which is useful for:
 * - Debugging workflow execution
 * - Validation and error reporting
 * - UI visualization (showing which nodes are tool-only, etc.)
 */
final class ExcludedNode {

  /**
   * Exclusion reason: Node only connected via tool_availability edges.
   */
  public const REASON_TOOL_ONLY = "tool_only";

  /**
   * Exclusion reason: Node has no edges (orphan).
   */
  public const REASON_NO_EDGES = "no_edges";

  /**
   * Exclusion reason: Node only connected via loopback edges.
   */
  public const REASON_LOOPBACK_ONLY = "loopback_only";

  /**
   * Exclusion reason: Node only connected via agent_result edges.
   */
  public const REASON_AGENT_RESULT_ONLY = "agent_result_only";

  /**
   * Constructs a new ExcludedNode.
   *
   * @param string $id
   *   The node ID.
   * @param string $reason
   *   The reason for exclusion (use REASON_* constants).
   * @param string $connectedVia
   *   The edge type that caused exclusion (if applicable).
   * @param string|null $parentNode
   *   The parent node ID (e.g., agent for tool nodes).
   * @param array<string, mixed> $metadata
   *   Additional metadata about the exclusion.
   */
  public function __construct(
    private readonly string $id,
    private readonly string $reason,
    private readonly string $connectedVia,
    private readonly ?string $parentNode = NULL,
    private readonly array $metadata = [],
  ) {}

  /**
   * Creates an ExcludedNode for a tool-only node.
   *
   * @param string $nodeId
   *   The node ID.
   * @param string $agentNodeId
   *   The agent node ID this tool belongs to.
   * @param array<string, mixed> $metadata
   *   Additional metadata.
   *
   * @return self
   *   A new ExcludedNode instance.
   */
  public static function forToolOnly(
    string $nodeId,
    string $agentNodeId,
    array $metadata = [],
  ): self {
    return new self(
      id: $nodeId,
      reason: self::REASON_TOOL_ONLY,
      connectedVia: DependencyEdge::TYPE_TOOL_AVAILABILITY,
      parentNode: $agentNodeId,
      metadata: $metadata,
    );
  }

  /**
   * Creates an ExcludedNode for an orphan node (no edges).
   *
   * @param string $nodeId
   *   The node ID.
   * @param array<string, mixed> $metadata
   *   Additional metadata.
   *
   * @return self
   *   A new ExcludedNode instance.
   */
  public static function forNoEdges(string $nodeId, array $metadata = []): self {
    return new self(
      id: $nodeId,
      reason: self::REASON_NO_EDGES,
      connectedVia: "",
      parentNode: NULL,
      metadata: $metadata,
    );
  }

  /**
   * Creates an ExcludedNode for a loopback-only node.
   *
   * @param string $nodeId
   *   The node ID.
   * @param string|null $parentNode
   *   The parent node ID.
   * @param array<string, mixed> $metadata
   *   Additional metadata.
   *
   * @return self
   *   A new ExcludedNode instance.
   */
  public static function forLoopbackOnly(
    string $nodeId,
    ?string $parentNode = NULL,
    array $metadata = [],
  ): self {
    return new self(
      id: $nodeId,
      reason: self::REASON_LOOPBACK_ONLY,
      connectedVia: DependencyEdge::TYPE_LOOPBACK,
      parentNode: $parentNode,
      metadata: $metadata,
    );
  }

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

  /**
   * Gets the exclusion reason.
   *
   * @return string
   *   The exclusion reason.
   */
  public function getReason(): string {
    return $this->reason;
  }

  /**
   * Gets the edge type that caused exclusion.
   *
   * @return string
   *   The edge type (may be empty for orphan nodes).
   */
  public function getConnectedVia(): string {
    return $this->connectedVia;
  }

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

  /**
   * Gets the metadata.
   *
   * @return array<string, mixed>
   *   The metadata.
   */
  public function getMetadata(): array {
    return $this->metadata;
  }

  /**
   * Gets a specific metadata value.
   *
   * @param string $key
   *   The metadata key.
   * @param mixed $default
   *   The default value if key doesn't exist.
   *
   * @return mixed
   *   The metadata value.
   */
  public function getMetadataValue(string $key, mixed $default = NULL): mixed {
    return $this->metadata[$key] ?? $default;
  }

  /**
   * Checks if this node was excluded because it's tool-only.
   *
   * @return bool
   *   TRUE if excluded as tool-only.
   */
  public function isToolOnly(): bool {
    return $this->reason === self::REASON_TOOL_ONLY;
  }

  /**
   * Checks if this node was excluded because it's an orphan.
   *
   * @return bool
   *   TRUE if excluded as orphan.
   */
  public function isOrphan(): bool {
    return $this->reason === self::REASON_NO_EDGES;
  }

  /**
   * Converts to array format.
   *
   * @return array<string, mixed>
   *   Array representation.
   */
  public function toArray(): array {
    return [
      "id" => $this->id,
      "reason" => $this->reason,
      "connectedVia" => $this->connectedVia,
      "parentNode" => $this->parentNode,
      "metadata" => $this->metadata,
    ];
  }

}
