<?php

declare(strict_types=1);

namespace Drupal\flowdrop_runtime\Service\Compiler;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolPassthroughInterface;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ToolsAwareInterface;
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 Psr\Log\LoggerInterface;

/**
 * Default {@see ToolConsumerValidatorInterface} implementation.
 *
 * Resolves a consumer node's processor class via the node type entity and the
 * processor plugin definition, then checks for {@see ToolsAwareInterface} or
 * {@see ToolPassthroughInterface} — a passthrough node (ToolBox) legitimately
 * receives tool edges, forwarding them rather than consuming them.
 */
final class ToolConsumerValidator implements ToolConsumerValidatorInterface {

  /**
   * Constructs a ToolConsumerValidator.
   *
   * @param \Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager $nodeProcessorManager
   *   The node processor plugin manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @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 validate(DependencyGraph $graph): array {
    $errors = [];
    $cache = [];
    foreach ($graph->getEdgesByType(DependencyEdge::TYPE_TOOL_AVAILABILITY) as $edge) {
      // Tool edges are drawn tool -> consumer, so the consumer is the target.
      $consumerId = $edge->getTarget();
      if (!array_key_exists($consumerId, $cache)) {
        $cache[$consumerId] = $this->consumerAcceptsTools($consumerId, $graph);
      }
      // Only flag a definitive negative; NULL means "could not determine".
      if ($cache[$consumerId] === FALSE) {
        $errors[$consumerId] = sprintf(
          'Node "%s" has tools wired to it, but its processor neither consumes tools (ToolsAwareInterface) nor forwards them (ToolPassthroughInterface); the tools would be ignored.',
          $consumerId,
        );
      }
    }
    return array_values($errors);
  }

  /**
   * Determines whether a node's processor legitimately accepts tool edges.
   *
   * A target accepts tool edges when it consumes tools (ToolsAwareInterface)
   * or forwards them (ToolPassthroughInterface, e.g. a ToolBox).
   *
   * @param string $nodeId
   *   The node ID.
   * @param \Drupal\flowdrop_runtime\DTO\Compiler\DependencyGraph $graph
   *   The dependency graph.
   *
   * @return bool|null
   *   TRUE/FALSE when resolved, NULL when the processor class is unknown.
   */
  private function consumerAcceptsTools(string $nodeId, DependencyGraph $graph): ?bool {
    $node = $graph->getNode($nodeId);
    if ($node === NULL) {
      return NULL;
    }

    try {
      $entity = $this->entityTypeManager
        ->getStorage("flowdrop_node_type")
        ->load($node->getTypeId());
      if (!$entity instanceof FlowDropNodeTypeInterface) {
        return NULL;
      }

      $definition = $this->nodeProcessorManager->getDefinition($entity->getExecutorPlugin(), FALSE);
      if (!is_array($definition) || !isset($definition["class"])) {
        return NULL;
      }

      $class = $definition["class"];
      $implements = class_implements($class) ?: [];
      return is_subclass_of($class, ToolsAwareInterface::class)
        || in_array(ToolsAwareInterface::class, $implements, TRUE)
        || is_subclass_of($class, ToolPassthroughInterface::class)
        || in_array(ToolPassthroughInterface::class, $implements, TRUE);
    }
    catch (\Exception $e) {
      $this->logger->warning("Could not resolve processor for node @node: @msg", [
        "@node" => $nodeId,
        "@msg" => $e->getMessage(),
      ]);
      return NULL;
    }
  }

}
