<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_processor\Plugin\FlowDropNodeProcessor;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop\Attribute\FlowDropNodeProcessor;
use Drupal\flowdrop\DTO\ParameterBagInterface;
use Drupal\flowdrop\DTO\ValidationResult;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\AbstractFlowDropNodeProcessor;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\DynamicPortTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Merge node processor.
 *
 * Exposes several user-defined input ports and combines their incoming values
 * into a single output. Each input is its own port (one edge each), so the
 * standard last-executor-wins multi-source resolution never applies — there is
 * no collision to resolve. The combination happens here, and the output is one
 * ordinary value that flows downstream as a single port.
 *
 * Two output modes (configurable per node instance):
 * - array (default): an ordered list of the input values, ordered by the
 *   declaration order of the input ports. Port names have no effect on the
 *   payload (they still label the handles and must be unique).
 * - object: a map keyed by input port name.
 *
 * This handles design-time-known arity ("merge these N branches"). It does NOT
 * collect a runtime-variable number of values arriving at a single port — that
 * is a different (unsolved) fan-in case, normally served by loop/ForEach
 * accumulation.
 *
 * @internal This class is not part of the public API and may change without
 *   notice.
 */
#[FlowDropNodeProcessor(
  id: "merge",
  label: new TranslatableMarkup("Merge"),
  description: "Combine several named inputs into one array or object",
  version: "1.0.0"
)]
class MergeNode extends AbstractFlowDropNodeProcessor {

  use DynamicPortTrait;

  /**
   * Config parameter selecting the output shape.
   */
  protected const OUTPUT_MODE_PARAM = "output_mode";

  /**
   * Output mode: ordered array of input values.
   */
  protected const OUTPUT_MODE_ARRAY = "array";

  /**
   * Output mode: map keyed by input port name.
   */
  protected const OUTPUT_MODE_OBJECT = "object";

  /**
   * Default output mode.
   */
  protected const DEFAULT_OUTPUT_MODE = self::OUTPUT_MODE_ARRAY;

  /**
   * {@inheritdoc}
   */
  public static function create(
    ContainerInterface $container,
    array $configuration,
    $plugin_id,
    $plugin_definition,
  ): static {
    return new static($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public function validateParams(array $params): ValidationResult {
    return ValidationResult::success();
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, mixed>
   *   A single "merged" key holding either an ordered list (array mode) or a
   *   name-keyed map (object mode) of the connected input values.
   */
  public function process(ParameterBagInterface $params): array {
    [$inputDefs] = $this->extractDynamicPortDefs($params);
    // No dynamic outputs: this node has a single static "merged" output port.
    $this->validatePortNames($inputDefs, []);
    $values = $this->collectDynamicInputValues($params);

    $mode = $params->getString(self::OUTPUT_MODE_PARAM, self::DEFAULT_OUTPUT_MODE);

    if ($mode === self::OUTPUT_MODE_OBJECT) {
      return ["merged" => $values];
    }

    // Array mode: ordered by the declaration order of the input ports.
    // Unconnected ports keep a NULL placeholder so indices stay aligned to
    // port order. Building $ordered by append only ($ordered[] = …) keeps it a
    // sequentially-keyed list, which is what makes Json::encode() emit a JSON
    // array (it keys array-vs-object off key sequentiality) — a gapped array
    // would serialize to an object and silently break "array" mode.
    $ordered = [];
    foreach ($inputDefs as $def) {
      if (!is_array($def)) {
        continue;
      }
      $name = $def["name"] ?? "";
      if ($name === "") {
        continue;
      }
      $ordered[] = $values[$name] ?? NULL;
    }

    return ["merged" => $ordered];
  }

  /**
   * {@inheritdoc}
   */
  public function getParameterSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        self::OUTPUT_MODE_PARAM => [
          "type" => "string",
          "title" => "Output Mode",
          "description" => "Combine inputs as an ordered array or a keyed object.",
          "default" => self::DEFAULT_OUTPUT_MODE,
          "enum" => [self::OUTPUT_MODE_ARRAY, self::OUTPUT_MODE_OBJECT],
        ],
        // Inputs only — the single output port is static (see getOutputSchema).
        ...$this->buildDynamicInputsSchema(),
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getOutputSchema(): array {
    return [
      "type" => "object",
      "properties" => [
        "merged" => [
          // Declared structured so downstream connections accept it in either
          // output mode (array list or keyed object).
          "type" => "object",
          "title" => "Merged",
          "description" => "The combined inputs — an array or object per the output mode.",
        ],
      ],
    ];
  }

}
