<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\flowdrop\Service\Bundle\BundleVerifier;
use Drupal\flowdrop_node_type\Service\NodeMetadataResolver;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;

/**
 * Assembles a portable, unsigned bundle envelope from a saved workflow.
 *
 * This is the single choke-point for export-side bundle assembly, shared by
 * the REST API and the drush export command so both emit an identical
 * envelope. The result is always unsigned (`signature => NULL`): signing is a
 * publisher operation performed off-site with a private key, never here.
 *
 * @internal
 *   This class is not part of the public API and may change without notice.
 */
final class BundleExporter {

  /**
   * The bundle envelope format identifier.
   *
   * Single-sourced from BundleVerifier so the exporter and the import/verify
   * side can never drift to different version strings.
   */
  public const FORMAT = BundleVerifier::FORMAT;

  /**
   * Constructs the BundleExporter.
   *
   * @param \Drupal\flowdrop_node_type\Service\NodeMetadataResolver $metadataResolver
   *   Resolves each node's executor plugin from its node type entity, so the
   *   exported `requires` surface is derived from the authoritative definition
   *   rather than the stored metadata cache (dropped from slim storage).
   */
  public function __construct(
    private readonly NodeMetadataResolver $metadataResolver,
  ) {}

  /**
   * Builds an unsigned bundle envelope for a workflow.
   *
   * @param \Drupal\flowdrop_workflow\WorkflowDefinitionInterface $workflow
   *   The workflow to export.
   *
   * @return array<string, mixed>
   *   The unsigned bundle: 'format', 'payload', and a NULL 'signature'.
   */
  public function export(WorkflowDefinitionInterface $workflow): array {
    // A bundle is a portable transport artifact for sites that may not have
    // these node types installed, so it must be self-describing. Slim storage
    // drops the derived metadata (executor plugin, ports); re-inject it from
    // this site's node types — which always exist for a flow authored here — so
    // the importing side's capability preflight can read it. Storage stays
    // slim; transport is fat. The strip re-slims it on import save.
    $nodes = $this->metadataResolver->enrichNodes($workflow->getNodes());

    // The distinct executor plugins the flow needs; the import-side capability
    // manifest re-derives this, but stamping it lets a publisher or human
    // reviewer see the dependency surface without parsing nodes.
    $requires = [];
    foreach ($nodes as $node) {
      $pluginId = (string) ($node['data']['metadata']['executor_plugin'] ?? '');
      if ($pluginId !== '' && !in_array($pluginId, $requires, TRUE)) {
        $requires[] = $pluginId;
      }
    }

    return [
      'format' => self::FORMAT,
      'payload' => [
        'publisher' => '',
        'requires' => $requires,
        'flow' => [
          'id' => $workflow->id(),
          'label' => $workflow->label(),
          'description' => $workflow->getDescription(),
          'nodes' => $nodes,
          'edges' => $workflow->getEdges(),
          'input_ports' => $workflow->getInputPorts(),
          'output_ports' => $workflow->getOutputPorts(),
          'metadata' => $workflow->getMetadata(),
        ],
      ],
      'signature' => NULL,
    ];
  }

}
