<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\DTO\Compiler;

/**
 * Represents the execution flow of a workflow.
 *
 * Contains only nodes that should be executed by the orchestrator.
 * This graph excludes:
 * - Tool-only nodes (connected only via tool_availability edges)
 * - Orphan nodes (nodes with no edges)
 * - Nodes connected only via special edges (loopback, agent_result)
 *
 * The execution order is determined by:
 * 1. Trigger edges (highest priority)
 * 2. Data edges (if no trigger edges)
 *
 * Data resolution at runtime uses the DependencyGraph, not this graph.
 *
 * @internal This class is not part of the public API and may change without notice.
 */
final class ExecutionGraph {

  /**
   * Constructs a new ExecutionGraph.
   *
   * @param array<string, ExecutionNode> $nodes
   *   Nodes to execute, keyed by node ID.
   * @param array<string> $executionOrder
   *   Topologically sorted execution order (node IDs).
   * @param array<string, ExcludedNode> $excludedNodes
   *   Nodes excluded from execution with reasons.
   * @param array<string, mixed> $metadata
   *   Graph metadata.
   */
  public function __construct(
    private readonly array $nodes,
    private readonly array $executionOrder,
    private readonly array $excludedNodes,
    private readonly array $metadata = [],
  ) {}

  /**
   * Gets the topologically sorted execution order.
   *
   * Nodes should be executed in this order to respect dependencies.
   *
   * @return array<string>
   *   Array of node IDs in execution order.
   */
  public function getExecutionOrder(): array {
    return $this->executionOrder;
  }

  /**
   * Gets all executable nodes.
   *
   * @return array<string, ExecutionNode>
   *   All nodes keyed by node ID.
   */
  public function getNodes(): array {
    return $this->nodes;
  }

  /**
   * Gets a specific executable node.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\ExecutionNode|null
   *   The node or NULL if not found.
   */
  public function getNode(string $nodeId): ?ExecutionNode {
    return $this->nodes[$nodeId] ?? NULL;
  }

  /**
   * Gets root nodes (nodes with no trigger dependencies).
   *
   * These nodes execute first as they don't wait for any triggers.
   *
   * @return array<string>
   *   Array of root node IDs.
   */
  public function getRootNodes(): array {
    $roots = [];
    foreach ($this->nodes as $nodeId => $node) {
      if ($node->isRoot()) {
        $roots[] = $nodeId;
      }
    }
    return $roots;
  }

  /**
   * Gets leaf nodes (nodes with no dependents in execution graph).
   *
   * @return array<string>
   *   Array of leaf node IDs.
   */
  public function getLeafNodes(): array {
    $leaves = [];
    foreach ($this->nodes as $nodeId => $node) {
      if ($node->isLeaf()) {
        $leaves[] = $nodeId;
      }
    }
    return $leaves;
  }

  /**
   * Gets nodes with special handling requirements.
   *
   * @param string|null $specialType
   *   Optional filter by special type ("agent", "iterator").
   *
   * @return array<string>
   *   Array of node IDs requiring special handling.
   */
  public function getSpecialNodes(?string $specialType = NULL): array {
    $special = [];
    foreach ($this->nodes as $nodeId => $node) {
      if ($node->requiresSpecialHandling()) {
        if ($specialType === NULL || $node->getSpecialType() === $specialType) {
          $special[] = $nodeId;
        }
      }
    }
    return $special;
  }

  /**
   * Gets agent nodes.
   *
   * @return array<string>
   *   Array of agent node IDs.
   */
  public function getAgentNodes(): array {
    return $this->getSpecialNodes(ExecutionNode::SPECIAL_TYPE_AGENT);
  }

  /**
   * Gets iterator nodes.
   *
   * @return array<string>
   *   Array of iterator node IDs.
   */
  public function getIteratorNodes(): array {
    return $this->getSpecialNodes(ExecutionNode::SPECIAL_TYPE_ITERATOR);
  }

  /**
   * Gets nodes excluded from execution.
   *
   * @return array<string, ExcludedNode>
   *   Excluded nodes keyed by node ID.
   */
  public function getExcludedNodes(): array {
    return $this->excludedNodes;
  }

  /**
   * Gets a specific excluded node.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\ExcludedNode|null
   *   The excluded node info or NULL if not found.
   */
  public function getExcludedNode(string $nodeId): ?ExcludedNode {
    return $this->excludedNodes[$nodeId] ?? NULL;
  }

  /**
   * Gets excluded nodes by reason.
   *
   * @param string $reason
   *   The exclusion reason (use ExcludedNode::REASON_* constants).
   *
   * @return array<string, ExcludedNode>
   *   Excluded nodes matching the reason.
   */
  public function getExcludedNodesByReason(string $reason): array {
    return array_filter(
      $this->excludedNodes,
      fn(ExcludedNode $node) => $node->getReason() === $reason
    );
  }

  /**
   * Gets tool-only nodes.
   *
   * Excluded because only connected via tool_availability.
   *
   * @return array<string, ExcludedNode>
   *   Tool-only excluded nodes.
   */
  public function getToolOnlyNodes(): array {
    return $this->getExcludedNodesByReason(ExcludedNode::REASON_TOOL_ONLY);
  }

  /**
   * Checks if a node is in the execution graph.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return bool
   *   TRUE if the node is in the execution graph.
   */
  public function hasNode(string $nodeId): bool {
    return isset($this->nodes[$nodeId]);
  }

  /**
   * Checks if a node is excluded.
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return bool
   *   TRUE if the node is excluded from execution.
   */
  public function isExcluded(string $nodeId): bool {
    return isset($this->excludedNodes[$nodeId]);
  }

  /**
   * Gets the graph metadata.
   *
   * @return array<string, mixed>
   *   The graph 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;
  }

  /**
   * Gets the total count of executable nodes.
   *
   * @return int
   *   The number of nodes in the execution graph.
   */
  public function getNodeCount(): int {
    return count($this->nodes);
  }

  /**
   * Gets the total count of excluded nodes.
   *
   * @return int
   *   The number of excluded nodes.
   */
  public function getExcludedCount(): int {
    return count($this->excludedNodes);
  }

  /**
   * Checks if the execution graph is empty.
   *
   * @return bool
   *   TRUE if there are no nodes to execute.
   */
  public function isEmpty(): bool {
    return empty($this->nodes);
  }

  /**
   * Gets nodes that depend on a specific node (dependents).
   *
   * @param string $nodeId
   *   The node ID.
   *
   * @return array<string>
   *   Array of dependent node IDs.
   */
  public function getDependents(string $nodeId): array {
    $dependents = [];
    foreach ($this->nodes as $candidateId => $node) {
      if (in_array($nodeId, $node->getTriggerDependencies(), TRUE)) {
        $dependents[] = $candidateId;
      }
    }
    return $dependents;
  }

  /**
   * Converts to array format.
   *
   * @return array<string, mixed>
   *   Array representation of the graph.
   */
  public function toArray(): array {
    $nodesArray = [];
    foreach ($this->nodes as $nodeId => $node) {
      $nodesArray[$nodeId] = $node->toArray();
    }

    $excludedArray = [];
    foreach ($this->excludedNodes as $nodeId => $excluded) {
      $excludedArray[$nodeId] = $excluded->toArray();
    }

    return [
      "nodes" => $nodesArray,
      "executionOrder" => $this->executionOrder,
      "excludedNodes" => $excludedArray,
      "metadata" => $this->metadata,
    ];
  }

}
