<?php

declare(strict_types=1);

namespace Drupal\flowdrop\Plugin\FlowDropNodeProcessor;

use Drupal\flowdrop\Constants\ReservedName;
use Drupal\flowdrop\DTO\ParameterBagInterface;

/**
 * Trait for node processors that support dynamic ports.
 *
 * Provides shared infrastructure for processors that allow workflow users
 * to define custom input and/or output ports per node instance. Use this
 * trait to avoid duplicating schema definitions and validation logic.
 *
 * Port names must be:
 * - Unique across ALL dynamic inputs and outputs (no duplicates allowed
 *   even between input and output ports)
 * - Valid identifiers: start with a letter, contain only [a-zA-Z0-9_]
 * - Not matching any reserved name (input, output, trigger, branches, etc.)
 * - Not starting with __ (reserved for internal system parameters)
 *
 * Static parameter names take precedence over dynamic port names. If a
 * processor has a static "data" parameter and a user creates a dynamic
 * input port also named "data", the static parameter receives the
 * connection value and the dynamic port is ignored.
 *
 * Usage:
 * @code
 * class MyProcessor extends AbstractFlowDropNodeProcessor {
 *   use DynamicPortTrait;
 *
 *   public function getParameterSchema(): array {
 *     return [
 *       "type" => "object",
 *       "properties" => [
 *         ...$this->buildDynamicInputsSchema(),
 *         ...$this->buildDynamicOutputsSchema(),
 *       ],
 *     ];
 *   }
 *
 *   public function process(ParameterBagInterface $params): array {
 *     [$inputDefs, $outputDefs] = $this->extractDynamicPortDefs($params);
 *     $this->validatePortNames($inputDefs, $outputDefs);
 *     $inputValues = $this->collectDynamicInputValues($params);
 *     // ... use $inputValues ...
 *   }
 * }
 * @endcode
 *
 * @see docs/development/reserved-names.md
 *
 * @api
 */
trait DynamicPortTrait {

  /**
   * Standard data types available for dynamic ports.
   *
   * @var array<int, string>
   */
  protected const DYNAMIC_PORT_DATA_TYPES = [
    "string", "number", "boolean", "array",
    "json", "mixed", "file", "image",
  ];

  /**
   * Build the dynamicInputs schema property for getParameterSchema().
   *
   * Returns a keyed array suitable for spreading into the schema's
   * "properties" key: `...$this->buildDynamicInputsSchema()`.
   *
   * @return array<string, array<string, mixed>>
   *   Array with single key "dynamicInputs" containing the JSON Schema.
   */
  protected function buildDynamicInputsSchema(): array {
    return [
      ReservedName::DYNAMIC_INPUTS => [
        "type" => "array",
        "title" => "Input Ports",
        "description" => "Define the input ports for receiving data.",
        "default" => [],
        "items" => [
          "type" => "object",
          "title" => "Input Port",
          "properties" => [
            "name" => [
              "type" => "string",
              "title" => "Port ID",
              "description" => "Unique identifier for this input port.",
            ],
            "label" => [
              "type" => "string",
              "title" => "Label",
              "description" => "Display label for this input.",
            ],
            "description" => [
              "type" => "string",
              "title" => "Description",
              "description" => "Description of the input.",
            ],
            "dataType" => [
              "type" => "string",
              "title" => "Data Type",
              "description" => "Expected data type.",
              "default" => "mixed",
              "enum" => static::DYNAMIC_PORT_DATA_TYPES,
            ],
            "required" => [
              "type" => "boolean",
              "title" => "Required",
              "description" => "Is this input required?",
              "default" => FALSE,
            ],
          ],
          "required" => ["name", "label", "dataType"],
        ],
      ],
    ];
  }

  /**
   * Build the dynamicOutputs schema property for getParameterSchema().
   *
   * Returns a keyed array suitable for spreading into the schema's
   * "properties" key: `...$this->buildDynamicOutputsSchema()`.
   *
   * @param array<string, array<string, mixed>> $extraProperties
   *   Additional JSON Schema properties to add to each output port item.
   *   For example, a Data Mapper adds a 'value' property for Twig
   *   expressions:
   *   @code
   *   $this->buildDynamicOutputsSchema([
   *     "value" => [
   *       "type" => "string",
   *       "title" => "Expression",
   *       "format" => "template",
   *     ],
   *   ]);
   *   @endcode
   *
   * @return array<string, array<string, mixed>>
   *   Array with single key "dynamicOutputs" containing the JSON Schema.
   */
  protected function buildDynamicOutputsSchema(array $extraProperties = []): array {
    $itemProperties = [
      "name" => [
        "type" => "string",
        "title" => "Port ID",
        "description" => "Unique identifier for this output port.",
      ],
      "label" => [
        "type" => "string",
        "title" => "Label",
        "description" => "Display label for this output.",
      ],
      "description" => [
        "type" => "string",
        "title" => "Description",
        "description" => "Description of the output.",
      ],
      "dataType" => [
        "type" => "string",
        "title" => "Data Type",
        "description" => "Output data type.",
        "default" => "mixed",
        "enum" => static::DYNAMIC_PORT_DATA_TYPES,
      ],
      "required" => [
        "type" => "boolean",
        "title" => "Required",
        "description" => "Is this output always provided?",
        "default" => FALSE,
      ],
    ];

    // Merge extra properties from the consuming processor.
    $itemProperties = array_merge($itemProperties, $extraProperties);

    return [
      ReservedName::DYNAMIC_OUTPUTS => [
        "type" => "array",
        "title" => "Output Ports",
        "description" => "Define the output ports for transformed data.",
        "default" => [],
        "items" => [
          "type" => "object",
          "title" => "Output Port",
          "properties" => $itemProperties,
          "required" => ["name", "label", "dataType"],
        ],
      ],
    ];
  }

  /**
   * Extract dynamic port definitions from the ParameterBag.
   *
   * @param \Drupal\flowdrop\DTO\ParameterBagInterface $params
   *   The resolved parameters.
   *
   * @return array{0: array<int, array<string, mixed>>, 1: array<int, array<string, mixed>>}
   *   A tuple of [inputDefs, outputDefs].
   */
  protected function extractDynamicPortDefs(ParameterBagInterface $params): array {
    $inputDefs = $params->get(ReservedName::DYNAMIC_INPUTS, []);
    $outputDefs = $params->get(ReservedName::DYNAMIC_OUTPUTS, []);

    if (!is_array($inputDefs)) {
      $inputDefs = [];
    }
    if (!is_array($outputDefs)) {
      $outputDefs = [];
    }

    return [$inputDefs, $outputDefs];
  }

  /**
   * Collect dynamic input values from the ParameterBag.
   *
   * Reads the dynamicInputs definitions and retrieves matching values.
   * Unconnected ports will have NULL values.
   *
   * @param \Drupal\flowdrop\DTO\ParameterBagInterface $params
   *   The resolved parameters containing both definitions and values.
   *
   * @return array<string, mixed>
   *   Input values keyed by port name.
   */
  protected function collectDynamicInputValues(ParameterBagInterface $params): array {
    $dynamicInputDefs = $params->get(ReservedName::DYNAMIC_INPUTS, []);
    if (!is_array($dynamicInputDefs)) {
      return [];
    }

    $inputValues = [];
    foreach ($dynamicInputDefs as $def) {
      if (!is_array($def)) {
        continue;
      }
      $name = $def["name"] ?? "";
      if ($name !== "") {
        $inputValues[$name] = $params->get($name);
      }
    }

    return $inputValues;
  }

  /**
   * Validate dynamic port names.
   *
   * Ensures names are valid identifiers, don't collide with reserved
   * system names, and are unique across inputs and outputs.
   *
   * @param array<int, array<string, mixed>> $inputDefs
   *   Dynamic input definitions.
   * @param array<int, array<string, mixed>> $outputDefs
   *   Dynamic output definitions.
   *
   * @throws \RuntimeException
   *   When a port name is invalid, reserved, or duplicated.
   */
  protected function validatePortNames(array $inputDefs, array $outputDefs): void {
    $seen = [];

    foreach ([$inputDefs, $outputDefs] as $defs) {
      foreach ($defs as $def) {
        if (!is_array($def)) {
          continue;
        }
        $name = $def["name"] ?? "";
        if ($name === "") {
          continue;
        }

        // Check naming pattern: alphanumeric + underscores, starts with letter.
        if (!preg_match('/^[a-zA-Z][a-zA-Z0-9_]*$/', $name)) {
          throw new \RuntimeException(
            "Dynamic port name '{$name}' must start with a letter and contain only letters, numbers, and underscores."
          );
        }

        // Check reserved names.
        if (in_array($name, ReservedName::forbiddenPortNames(), TRUE)
            || str_starts_with($name, "__")) {
          throw new \RuntimeException(
            "Dynamic port name '{$name}' is reserved and cannot be used."
          );
        }

        // Check duplicates.
        if (isset($seen[$name])) {
          throw new \RuntimeException(
            "Duplicate dynamic port name '{$name}'."
          );
        }
        $seen[$name] = TRUE;
      }
    }
  }

}
