<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop\Utility\NodeAnchor;
use Drupal\flowdrop_workflow\DTO\NodeCapability;
use Drupal\flowdrop_workflow\DTO\WorkflowCapabilityManifest;

/**
 * Builds a capability manifest for a decoded (not-yet-saved) workflow.
 *
 * Read-only preflight analysis: for every distinct node type the workflow
 * references, it resolves the backing processor plugin and an existing node
 * type entity, then buckets the capability as available / generatable /
 * missing. It probes definitions only (no plugin instantiation) and writes
 * nothing.
 *
 * This is the import-time counterpart to the save-path WorkflowValidator: it
 * answers "what does this flow need, and can this site provide it?" rather than
 * "is this flow structurally valid?".
 *
 * @internal
 *   This class is not part of the public API and may change without notice.
 */
final class WorkflowCapabilityManifestBuilder {

  public function __construct(
    private readonly FlowDropNodeProcessorPluginManager $pluginManager,
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {}

  /**
   * Builds the capability manifest for a set of workflow nodes.
   *
   * @param array<int, array<string, mixed>> $nodes
   *   The decoded workflow nodes.
   *
   * @return \Drupal\flowdrop_workflow\DTO\WorkflowCapabilityManifest
   *   The manifest, one capability per distinct node type.
   */
  public function build(array $nodes): WorkflowCapabilityManifest {
    $storage = $this->entityTypeManager->getStorage('flowdrop_node_type');

    $capabilities = [];
    foreach ($nodes as $node) {
      $metadata = $node['data']['metadata'] ?? [];
      $nodeTypeId = NodeAnchor::readMetadata($metadata);
      $pluginId = (string) ($metadata['executor_plugin'] ?? '');

      // De-duplicate by node type id: many nodes can share one node type, and
      // the capability outcome is identical for each.
      if ($nodeTypeId === '' || isset($capabilities[$nodeTypeId])) {
        continue;
      }

      $capabilities[$nodeTypeId] = $this->resolveCapability($nodeTypeId, $pluginId, $storage->load($nodeTypeId) !== NULL);
    }

    return new WorkflowCapabilityManifest(array_values($capabilities));
  }

  /**
   * Resolves a single capability's status.
   *
   * @param string $nodeTypeId
   *   The node type id (metadata.node_type_id).
   * @param string $pluginId
   *   The declared executor plugin id (module:plugin).
   * @param bool $nodeTypeExists
   *   Whether a node type entity already exists for this id.
   *
   * @return \Drupal\flowdrop_workflow\DTO\NodeCapability
   *   The resolved capability.
   */
  private function resolveCapability(string $nodeTypeId, string $pluginId, bool $nodeTypeExists): NodeCapability {
    $providerModule = $this->providerPrefix($pluginId);

    if ($pluginId === '') {
      return new NodeCapability(
        NodeCapability::MISSING,
        $nodeTypeId,
        $pluginId,
        '',
        sprintf('Node type "%s" declares no executor plugin.', $nodeTypeId),
      );
    }

    $definition = $this->pluginManager->getDefinition($pluginId, FALSE);
    if (!is_array($definition)) {
      return new NodeCapability(
        NodeCapability::MISSING,
        $nodeTypeId,
        $pluginId,
        $providerModule,
        sprintf('Processor "%s" is not installed. Install the module "%s" that provides it.', $pluginId, $providerModule),
      );
    }

    // Anti-spoof: the namespace prefix must match the real provider module, so
    // a flow cannot attribute a plugin to a module it does not come from.
    $provider = (string) ($definition['provider'] ?? '');
    if ($providerModule === '' || $provider === '' || $providerModule !== $provider) {
      return new NodeCapability(
        NodeCapability::MISSING,
        $nodeTypeId,
        $pluginId,
        $provider,
        sprintf('Processor "%s" does not match its provider module "%s".', $pluginId, $provider),
      );
    }

    return new NodeCapability(
      $nodeTypeExists ? NodeCapability::AVAILABLE : NodeCapability::GENERATABLE,
      $nodeTypeId,
      $pluginId,
      $provider,
    );
  }

  /**
   * Extracts the provider-module prefix from a namespaced plugin id.
   *
   * @param string $pluginId
   *   The plugin id (module:plugin), or empty.
   *
   * @return string
   *   The module prefix, or empty when the id is not namespaced.
   */
  private function providerPrefix(string $pluginId): string {
    $position = strpos($pluginId, ':');
    return $position === FALSE ? '' : substr($pluginId, 0, $position);
  }

}
