<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\DTO\Compiler;

use Drupal\flowdrop_workflow\DTO\WorkflowDTO;

/**
 * Represents the complete dependency graph of a workflow.
 *
 * Contains all nodes and edges
 * (including special edges like tool_availability).
 * This graph is used for:
 * - Tool discovery by agents
 * - Data resolution at runtime
 * - Understanding complete workflow structure.
 *
 * Unlike the ExecutionGraph, this contains ALL connections regardless of
 * whether they affect execution order.
 */
final class DependencyGraph {

  /**
   * Constructs a new DependencyGraph.
   *
   * @param array<string, DependencyNode> $nodes
   *   All nodes keyed by node ID.
   * @param array<string, DependencyEdge> $edges
   *   All edges keyed by edge ID.
   * @param array<string, array<DependencyEdge>> $incomingEdgeIndex
   *   Edges indexed by target node ID (for fast parent lookups).
   * @param array<string, array<DependencyEdge>> $outgoingEdgeIndex
   *   Edges indexed by source node ID (for fast child lookups).
   * @param array<string, mixed> $metadata
   *   Graph metadata.
   */
  public function __construct(
    private readonly array $nodes,
    private readonly array $edges,
    private readonly array $incomingEdgeIndex,
    private readonly array $outgoingEdgeIndex,
    private readonly array $metadata = [],
  ) {}

  /**
   * Creates a DependencyGraph from a WorkflowDTO.
   *
   * @param \Drupal\flowdrop_workflow\DTO\WorkflowDTO $workflow
   *   The workflow DTO.
   *
   * @return self
   *   A new DependencyGraph instance.
   */
  public static function fromWorkflow(WorkflowDTO $workflow): self {
    // Build nodes.
    $nodes = [];
    foreach ($workflow->getNodes() as $nodeId => $node) {
      $nodes[$nodeId] = DependencyNode::fromWorkflowNode($node);
    }

    // Build edges and indexes.
    $edges = [];
    $incomingEdgeIndex = [];
    $outgoingEdgeIndex = [];

    // Initialize empty arrays for all nodes.
    foreach (array_keys($nodes) as $nodeId) {
      $incomingEdgeIndex[$nodeId] = [];
      $outgoingEdgeIndex[$nodeId] = [];
    }

    // Process all edges.
    foreach ($workflow->getEdges() as $edge) {
      $dependencyEdge = DependencyEdge::fromWorkflowEdge($edge);
      $edges[$dependencyEdge->getId()] = $dependencyEdge;

      $sourceId = $dependencyEdge->getSource();
      $targetId = $dependencyEdge->getTarget();

      // Add to outgoing index (source → target).
      if (isset($outgoingEdgeIndex[$sourceId])) {
        $outgoingEdgeIndex[$sourceId][] = $dependencyEdge;
      }

      // Add to incoming index (target ← source).
      if (isset($incomingEdgeIndex[$targetId])) {
        $incomingEdgeIndex[$targetId][] = $dependencyEdge;
      }
    }

    // Build metadata.
    $metadata = [
      "nodeCount" => count($nodes),
      "edgeCount" => count($edges),
      "workflowId" => $workflow->getId(),
      "workflowName" => $workflow->getName(),
    ];

    return new self(
      nodes: $nodes,
      edges: $edges,
      incomingEdgeIndex: $incomingEdgeIndex,
      outgoingEdgeIndex: $outgoingEdgeIndex,
      metadata: $metadata,
    );
  }

  /**
   * Gets parent nodes (nodes with edges pointing TO this node).
   *
   * Returns rich objects with edge metadata for each parent connection.
   *
   * @param string $nodeId
   *   The node ID to get parents for.
   * @param string|null $edgeType
   *   Optional filter by edge type (e.g., "data",
   *   "trigger", "tool_availability").
   *
   * @return array<array{nodeId: string, edgeId: string, edgeType: string, sourceHandle: string, targetHandle: string, sourcePortName: string, targetPortName: string, metadata: array<string, mixed>}>
   *   Array of parent connection details.
   */
  public function getParents(string $nodeId, ?string $edgeType = NULL): array {
    $incomingEdges = $this->incomingEdgeIndex[$nodeId] ?? [];
    $parents = [];

    foreach ($incomingEdges as $edge) {
      // Filter by edge type if specified.
      if ($edgeType !== NULL && $edge->getEdgeType() !== $edgeType) {
        continue;
      }

      $parents[] = [
        "nodeId" => $edge->getSource(),
        "edgeId" => $edge->getId(),
        "edgeType" => $edge->getEdgeType(),
        "sourceHandle" => $edge->getSourceHandle(),
        "targetHandle" => $edge->getTargetHandle(),
        "sourcePortName" => $edge->getSourcePortName(),
        "targetPortName" => $edge->getTargetPortName(),
        "metadata" => $edge->getMetadata(),
      ];
    }

    return $parents;
  }

  /**
   * Gets child nodes (nodes this node points TO).
   *
   * Returns rich objects with edge metadata for each child connection.
   *
   * @param string $nodeId
   *   The node ID to get children for.
   * @param string|null $edgeType
   *   Optional filter by edge type (e.g., "data",
   *   "trigger", "tool_availability").
   *
   * @return array<array{nodeId: string, edgeId: string, edgeType: string, sourceHandle: string, targetHandle: string, sourcePortName: string, targetPortName: string, metadata: array<string, mixed>}>
   *   Array of child connection details.
   */
  public function getChildren(string $nodeId, ?string $edgeType = NULL): array {
    $outgoingEdges = $this->outgoingEdgeIndex[$nodeId] ?? [];
    $children = [];

    foreach ($outgoingEdges as $edge) {
      // Filter by edge type if specified.
      if ($edgeType !== NULL && $edge->getEdgeType() !== $edgeType) {
        continue;
      }

      $children[] = [
        "nodeId" => $edge->getTarget(),
        "edgeId" => $edge->getId(),
        "edgeType" => $edge->getEdgeType(),
        "sourceHandle" => $edge->getSourceHandle(),
        "targetHandle" => $edge->getTargetHandle(),
        "sourcePortName" => $edge->getSourcePortName(),
        "targetPortName" => $edge->getTargetPortName(),
        "metadata" => $edge->getMetadata(),
      ];
    }

    return $children;
  }

  /**
   * Gets parent node IDs only (without edge metadata).
   *
   * @param string $nodeId
   *   The node ID.
   * @param string|null $edgeType
   *   Optional filter by edge type.
   *
   * @return array<string>
   *   Array of parent node IDs.
   */
  public function getParentIds(string $nodeId, ?string $edgeType = NULL): array {
    $parents = $this->getParents($nodeId, $edgeType);
    return array_unique(array_column($parents, "nodeId"));
  }

  /**
   * Gets child node IDs only (without edge metadata).
   *
   * @param string $nodeId
   *   The node ID.
   * @param string|null $edgeType
   *   Optional filter by edge type.
   *
   * @return array<string>
   *   Array of child node IDs.
   */
  public function getChildIds(string $nodeId, ?string $edgeType = NULL): array {
    $children = $this->getChildren($nodeId, $edgeType);
    return array_unique(array_column($children, "nodeId"));
  }

  /**
   * Gets incoming edges for a node.
   *
   * @param string $nodeId
   *   The node ID.
   * @param string|null $edgeType
   *   Optional filter by edge type.
   *
   * @return array<DependencyEdge>
   *   Array of incoming edges.
   */
  public function getIncomingEdges(string $nodeId, ?string $edgeType = NULL): array {
    $edges = $this->incomingEdgeIndex[$nodeId] ?? [];

    if ($edgeType === NULL) {
      return $edges;
    }

    return array_filter($edges, fn(DependencyEdge $e) => $e->getEdgeType() === $edgeType);
  }

  /**
   * Gets outgoing edges for a node.
   *
   * @param string $nodeId
   *   The node ID.
   * @param string|null $edgeType
   *   Optional filter by edge type.
   *
   * @return array<DependencyEdge>
   *   Array of outgoing edges.
   */
  public function getOutgoingEdges(string $nodeId, ?string $edgeType = NULL): array {
    $edges = $this->outgoingEdgeIndex[$nodeId] ?? [];

    if ($edgeType === NULL) {
      return $edges;
    }

    return array_filter($edges, fn(DependencyEdge $e) => $e->getEdgeType() === $edgeType);
  }

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

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

  /**
   * Gets a specific edge.
   *
   * @param string $edgeId
   *   The edge ID.
   *
   * @return \Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge|null
   *   The edge or NULL if not found.
   */
  public function getEdge(string $edgeId): ?DependencyEdge {
    return $this->edges[$edgeId] ?? NULL;
  }

  /**
   * Gets all edges.
   *
   * @return array<string, DependencyEdge>
   *   All edges keyed by edge ID.
   */
  public function getAllEdges(): array {
    return $this->edges;
  }

  /**
   * Gets all edges of a specific type.
   *
   * @param string $edgeType
   *   The edge type to filter by.
   *
   * @return array<string, DependencyEdge>
   *   Edges of the specified type.
   */
  public function getEdgesByType(string $edgeType): array {
    return array_filter($this->edges, fn(DependencyEdge $e) => $e->getEdgeType() === $edgeType);
  }

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

  /**
   * Checks if an edge exists.
   *
   * @param string $edgeId
   *   The edge ID.
   *
   * @return bool
   *   TRUE if the edge exists.
   */
  public function hasEdge(string $edgeId): bool {
    return isset($this->edges[$edgeId]);
  }

  /**
   * 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 node count.
   *
   * @return int
   *   The number of nodes.
   */
  public function getNodeCount(): int {
    return count($this->nodes);
  }

  /**
   * Gets the total edge count.
   *
   * @return int
   *   The number of edges.
   */
  public function getEdgeCount(): int {
    return count($this->edges);
  }

  /**
   * Gets root nodes (nodes with no incoming edges).
   *
   * @return array<string>
   *   Array of root node IDs.
   */
  public function getRootNodeIds(): array {
    $roots = [];
    foreach ($this->nodes as $nodeId => $node) {
      if (empty($this->incomingEdgeIndex[$nodeId])) {
        $roots[] = $nodeId;
      }
    }
    return $roots;
  }

  /**
   * Gets leaf nodes (nodes with no outgoing edges).
   *
   * @return array<string>
   *   Array of leaf node IDs.
   */
  public function getLeafNodeIds(): array {
    $leaves = [];
    foreach ($this->nodes as $nodeId => $node) {
      if (empty($this->outgoingEdgeIndex[$nodeId])) {
        $leaves[] = $nodeId;
      }
    }
    return $leaves;
  }

  /**
   * 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();
    }

    $edgesArray = [];
    foreach ($this->edges as $edgeId => $edge) {
      $edgesArray[$edgeId] = $edge->toArray();
    }

    return [
      "nodes" => $nodesArray,
      "edges" => $edgesArray,
      "metadata" => $this->metadata,
    ];
  }

}
