<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Tool;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolPassthroughInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyEdge;
use Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph;
use Drupal\flowdrop_runtime\Exception\ToolWiringException;
use Psr\Log\LoggerInterface;

/**
 * Default {@see ToolGraphFlattenerInterface} implementation.
 *
 * Walks a consumer's incoming tool_availability edges depth-first, recursing
 * through passthrough nodes (detected by processor class, never instantiated)
 * and collecting leaves deduplicated by node ID. The recursion carries the
 * current path for cycle detection and a passthrough-nesting depth capped at
 * {@see self::MAX_PASSTHROUGH_DEPTH}.
 */
final class ToolGraphFlattener implements ToolGraphFlattenerInterface {

  /**
   * Maximum passthrough (ToolBox) nesting depth.
   *
   * A backstop, far above any sane authoring; cycles are detected separately
   * and exactly.
   */
  public const MAX_PASSTHROUGH_DEPTH = 8;

  /**
   * Cache of node type ID → passthrough?.
   *
   * Passthrough-ness is a property of the node type's processor class, so the
   * cache is valid across graphs for the lifetime of the service.
   *
   * @var array<string, bool>
   */
  private array $passthroughCache = [];

  /**
   * Constructs a ToolGraphFlattener.
   *
   * @param \Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager $nodeProcessorManager
   *   The node processor plugin manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager (for loading node type entities).
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger.
   */
  public function __construct(
    private readonly FlowDropNodeProcessorPluginManager $nodeProcessorManager,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly LoggerInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function flattenLeafTools(string $consumerNodeId, DependencyGraph $graph): array {
    $leaves = [];
    $this->collect($consumerNodeId, $graph, $leaves, [$consumerNodeId], 0);
    return array_keys($leaves);
  }

  /**
   * {@inheritdoc}
   */
  public function isPassthrough(string $nodeId, DependencyGraph $graph): bool {
    $node = $graph->getNode($nodeId);
    if ($node === NULL) {
      return FALSE;
    }
    $nodeTypeId = $node->getNodeTypeId();
    if (isset($this->passthroughCache[$nodeTypeId])) {
      return $this->passthroughCache[$nodeTypeId];
    }

    $result = FALSE;
    try {
      $entity = $this->entityTypeManager
        ->getStorage("flowdrop_node_type")
        ->load($nodeTypeId);
      if ($entity instanceof FlowDropNodeTypeInterface) {
        $definition = $this->nodeProcessorManager->getDefinition($entity->getExecutorPlugin(), FALSE);
        if (is_array($definition) && isset($definition["class"])) {
          $class = $definition["class"];
          $result = is_subclass_of($class, ToolPassthroughInterface::class)
            || in_array(ToolPassthroughInterface::class, class_implements($class) ?: [], TRUE);
        }
      }
    }
    catch (\Exception $e) {
      // Unresolvable nodes are conservatively treated as plain tools; any
      // real problem surfaces when the tool is described or invoked.
      $this->logger->warning("Could not resolve processor for node @node: @msg", [
        "@node" => $nodeId,
        "@msg" => $e->getMessage(),
      ]);
      // Do not cache failures — a later call may succeed.
      return FALSE;
    }

    return $this->passthroughCache[$nodeTypeId] = $result;
  }

  /**
   * Collects leaf tools of a node's incoming tool edges, depth-first.
   *
   * @param string $nodeId
   *   The node whose tool parents to walk.
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $graph
   *   The dependency graph.
   * @param array<string, true> $leaves
   *   Collected leaf node IDs (keys; preserves traversal order, dedupes).
   * @param array<int, string> $path
   *   The current traversal path, consumer first, for cycle reporting.
   * @param int $depth
   *   The current passthrough nesting depth.
   *
   * @throws \Drupal\flowdrop_runtime\Exception\ToolWiringException
   *   On a passthrough cycle or excessive nesting depth.
   */
  private function collect(
    string $nodeId,
    DependencyGraph $graph,
    array &$leaves,
    array $path,
    int $depth,
  ): void {
    $parentIds = $graph->getParentIds($nodeId, DependencyEdge::TYPE_TOOL_AVAILABILITY);

    if (empty($parentIds) && $depth > 0) {
      // An empty box is legal (e.g. under construction) but worth surfacing:
      // it contributes nothing to any consumer.
      $this->logger->warning("ToolBox @node has no tools wired into it.", [
        "@node" => $nodeId,
      ]);
      return;
    }

    foreach ($parentIds as $parentId) {
      if (in_array($parentId, $path, TRUE)) {
        throw new ToolWiringException(sprintf(
          "Tool wiring cycle detected: %s.",
          implode(" -> ", array_merge(array_slice($path, (int) array_search($parentId, $path, TRUE)), [$parentId])),
        ));
      }

      if (!$this->isPassthrough($parentId, $graph)) {
        // A leaf tool. Dedupe by node ID: the same tool reached via two boxes
        // is the same tool, not a collision.
        $leaves[$parentId] = TRUE;
        continue;
      }

      if ($depth + 1 > self::MAX_PASSTHROUGH_DEPTH) {
        throw new ToolWiringException(sprintf(
          "Tool passthrough nesting exceeds the maximum depth of %d at node \"%s\" (path: %s).",
          self::MAX_PASSTHROUGH_DEPTH,
          $parentId,
          implode(" -> ", array_merge($path, [$parentId])),
        ));
      }

      $this->collect($parentId, $graph, $leaves, array_merge($path, [$parentId]), $depth + 1);
    }
  }

}
