<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Form;

use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\flowdrop\Constants\ReservedName;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_type\Entity\FlowDropNodeType;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_node_type\Form\Trait\NodeTypeFormSectionsTrait;
use Drupal\flowdrop_node_type\Form\Trait\NodeTypePluginHelperTrait;
use Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * FlowDrop Node Type multi-step form with tabular parameter configuration.
 *
 * This form uses a two-step wizard approach with URL-based step management:
 *  - Step 1: Select the executor plugin (/admin/flowdrop/config/node-types/add)
 *  - Step 2: Configure settings
 *    (/admin/flowdrop/config/node-types/add?plugin=X)
 *
 * Using URL parameters avoids form state caching/serialization issues.
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see docs/development/unified-parameter-system-spec.md
 */
final class FlowDropNodeTypeForm extends EntityForm {

  use NodeTypePluginHelperTrait;
  use NodeTypeFormSectionsTrait;

  /**
   * The entity being edited.
   *
   * @var \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface
   */
  protected $entity;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * The node type manager service.
   *
   * @var \Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManagerInterface
   */
  protected FlowDropNodeTypeManagerInterface $nodeTypeManager;

  /**
   * The node executor plugin manager.
   *
   * @var \Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager
   */
  protected FlowDropNodeProcessorPluginManager $nodeProcessorPluginManager;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    $instance = parent::create($container);
    $instance->entityTypeManager = $container->get("entity_type.manager");
    $instance->nodeTypeManager = $container->get("flowdrop_node_type.manager");
    $instance->nodeProcessorPluginManager = $container->get("flowdrop.node_processor_plugin_manager");
    return $instance;
  }

  /**
   * Get the selected plugin from URL query parameter.
   *
   * @return string|null
   *   The plugin ID from URL, or NULL if not set.
   */
  protected function getPluginFromUrl(): ?string {
    $request = $this->getRequest();

    $plugin = $request->query->get("plugin");
    if (empty($plugin) || !is_string($plugin)) {
      return NULL;
    }

    // Validate plugin exists.
    $definition = $this->getPluginDefinition($plugin);
    return $definition !== NULL ? $plugin : NULL;
  }

  /**
   * Build a map of executor plugin IDs to the node types already using them.
   *
   * @return array<string, array<int, array{label: string, url: string}>>
   *   Keyed by plugin ID; each value is a list of existing node types with
   *   their label and edit-form URL.
   */
  protected function getNodeTypeUsageByPlugin(): array {
    $usage = [];

    try {
      $storage = $this->entityTypeManager->getStorage("flowdrop_node_type");
      foreach ($storage->loadMultiple() as $nodeType) {
        if (!$nodeType instanceof FlowDropNodeTypeInterface) {
          continue;
        }

        $plugin = $nodeType->getExecutorPlugin();
        if ($plugin === "") {
          continue;
        }

        $usage[$plugin][] = [
          "label" => (string) $nodeType->label(),
          "url" => $nodeType->toUrl("edit-form")->toString(),
        ];
      }
    }
    catch (\Exception $e) {
      // Usage data is a non-critical enhancement; degrade gracefully.
    }

    return $usage;
  }

  /**
   * Check if we're in step 2 (configuration step).
   *
   * @return bool
   *   TRUE if in configuration step, FALSE for plugin selection step.
   */
  protected function isConfigurationStep(): bool {
    // Edit form always goes directly to configuration.
    if (!$this->entity->isNew()) {
      return TRUE;
    }

    // For new entities, check URL parameter.
    return $this->getPluginFromUrl() !== NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function form(array $form, FormStateInterface $form_state): array {
    $form = parent::form($form, $form_state);

    if (!$this->entity instanceof FlowDropNodeTypeInterface) {
      return $form;
    }

    $form["#attached"]["library"][] = "flowdrop_node_type/admin";

    $isNewEntity = $this->entity->isNew();
    $isConfigStep = $this->isConfigurationStep();

    // Add step indicator for new entities.
    if ($isNewEntity) {
      $form["step_indicator"] = [
        "#type" => "markup",
        "#markup" => $this->buildStepIndicator($isConfigStep),
        "#weight" => -100,
      ];
    }

    // Build form based on current step.
    if (!$isConfigStep) {
      $this->buildPluginSelectionStep($form, $form_state);
    }
    else {
      $this->buildConfigurationStep($form, $form_state);
    }

    return $form;
  }

  /**
   * Build Step 1: Plugin Selection.
   *
   * Renders a grid-based plugin selector grouped by provider module,
   * displaying plugin title and description for easier selection.
   * Uses the flowdrop_ui_components:plugin-selector SDC component.
   *
   * @param array<string, mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  protected function buildPluginSelectionStep(array &$form, FormStateInterface $form_state): void {
    $groupedPlugins = $this->getPluginsGroupedByProvider();

    // Auto-advance to configuration as soon as a plugin is chosen.
    $form["#attached"]["library"][] = "flowdrop_node_type/plugin_selection_autosubmit";

    $form["plugin_section"] = [
      "#type" => "container",
      "#attributes" => [
        "class" => ["plugin-selection-container"],
      ],
    ];

    $form["plugin_section"]["title"] = [
      "#type" => "markup",
      "#markup" => '<h2 class="plugin-selection-title">' . $this->t("Step 1: Select Executor Plugin") . '</h2>',
    ];

    $form["plugin_section"]["description"] = [
      "#type" => "markup",
      "#markup" => '<p class="plugin-selection-description">' . $this->t("Select the executor plugin that will power this node type. The plugin determines what parameters and outputs are available for configuration.") . "</p>",
    ];

    // Map each plugin to the node types already using it, so the selector
    // can flag plugins that are already configured and avoid accidental
    // duplicates.
    $usageByPlugin = $this->getNodeTypeUsageByPlugin();

    // Transform grouped plugins to the format expected by the SDC component.
    $pluginGroups = $this->transformPluginsForComponent($groupedPlugins, $usageByPlugin);

    // Use the plugin-selector SDC component.
    // The component creates its own hidden input for UI purposes, but we need
    // a Drupal form API hidden field for proper form state handling.
    $form["plugin_section"]["plugin_selector"] = [
      "#type" => "component",
      "#component" => "flowdrop_ui_components:plugin-selector",
      "#props" => [
        "plugins" => $pluginGroups,
        "input_name" => "executor_plugin",
        "input_id" => "edit-executor-plugin",
        "selected" => "",
        "stagger" => TRUE,
      ],
    ];

    // Hidden field for Drupal form API to capture the selected plugin value.
    // The SDC component's JavaScript syncs the selection to this field.
    // Using #parents to ensure the value is captured at the root level.
    $form["plugin_section"]["executor_plugin"] = [
      "#type" => "hidden",
      "#default_value" => "",
      "#parents" => ["executor_plugin"],
      "#attributes" => [
        "id" => "edit-executor-plugin",
        "class" => ["fd-plugin-selector__form-input"],
      ],
    ];
  }

  /**
   * Build Step 2: Configuration.
   *
   * @param array<string, mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  protected function buildConfigurationStep(array &$form, FormStateInterface $form_state): void {
    $isNewEntity = $this->entity->isNew();

    // Get selected plugin from URL (for new entities) or entity.
    $selectedPlugin = $isNewEntity
      ? $this->getPluginFromUrl()
      : $this->entity->getExecutorPlugin();

    if (empty($selectedPlugin)) {
      $form["error"] = [
        "#type" => "markup",
        "#markup" => '<div class="messages messages--error">' .
        $this->t("No plugin selected. Please go back and select a plugin.") .
        "</div>",
      ];
      return;
    }

    // Get plugin definition for auto-fill.
    $pluginDefinition = $this->getPluginDefinition($selectedPlugin);

    // === PLUGIN INFO (Read-only display) ===
    $form["plugin_info"] = [
      "#type" => "fieldset",
      "#title" => $this->t("Selected Plugin"),
      "#weight" => -10,
    ];

    $pluginLabel = $pluginDefinition["label"] ?? $selectedPlugin;
    $pluginLabelStr = (string) $pluginLabel;

    $form["plugin_info"]["plugin_display"] = [
      "#type" => "item",
      "#title" => $this->t("Executor Plugin"),
      "#markup" => '<strong>' . Html::escape($pluginLabelStr) . '</strong> <code>(' . Html::escape($selectedPlugin) . ")</code>",
    ];

    // Store plugin ID as hidden field.
    $form["executor_plugin"] = [
      "#type" => "hidden",
      "#value" => $selectedPlugin,
    ];

    // === INTRO ===
    // A short, plain-language orientation for site builders who may not know
    // what a "node type" is or how it relates to the workflow editor.
    $form["intro"] = [
      "#type" => "markup",
      "#markup" => '<p class="section-description">' .
      $this->t("A node type is a reusable building block that authors drag onto a workflow canvas. Give it a name, choose how it looks, and save — the inputs and outputs below come from the selected plugin.") .
      "</p>",
      "#weight" => -20,
    ];

    // === BASICS SECTION ===
    $form["basics"] = [
      "#type" => "fieldset",
      "#title" => $this->t("Basics"),
    ];

    // Determine default values for new entities (auto-fill from plugin).
    if ($isNewEntity && $pluginDefinition !== NULL) {
      $labelDefault = (string) $pluginLabel;
      $descriptionDefault = $pluginDefinition["description"] ?? "";
      // Sanitize plugin ID to create a valid machine name.
      // Plugin IDs may contain colons (e.g., "flowdrop_ai_provider:chat_model")
      // which are not valid in Drupal machine names. Replace colons and other
      // invalid characters with underscores.
      $idDefault = preg_replace('/[^a-z0-9_]+/', '_', strtolower($selectedPlugin));
      // Remove leading/trailing underscores and collapse multiple underscores.
      $idDefault = trim(preg_replace('/_+/', '_', $idDefault), '_');
    }
    else {
      $labelDefault = $this->entity->label();
      $descriptionDefault = $this->entity->getDescription();
      $idDefault = $this->entity->id();
    }

    $form["basics"]["label"] = [
      "#type" => "textfield",
      "#title" => $this->t("Name"),
      "#description" => $this->t("Shown to authors in the node picker, e.g. 'JSON to Data'."),
      "#maxlength" => 255,
      "#default_value" => $labelDefault,
      "#required" => TRUE,
    ];

    $form["basics"]["id"] = [
      "#type" => "machine_name",
      "#default_value" => $idDefault,
      "#machine_name" => [
        "exists" => [FlowDropNodeType::class, "load"],
      ],
      "#disabled" => !$isNewEntity,
    ];

    $form["basics"]["description"] = [
      "#type" => "textarea",
      "#title" => $this->t("Description"),
      "#description" => $this->t("A short sentence telling authors what this node does and when to use it."),
      "#default_value" => $descriptionDefault,
      "#rows" => 2,
    ];

    $categories = $this->nodeTypeManager->getAvailableCategories();

    $form["basics"]["category"] = [
      "#type" => "select",
      "#title" => $this->t("Category"),
      "#description" => $this->t("Groups this node in the picker and sets its colour. Pick the group it fits best."),
      "#default_value" => $this->entity->getCategory(),
      "#options" => $categories,
      "#required" => TRUE,
      "#empty_option" => $this->t("- Select -"),
    ];

    $form["basics"]["tags_csv"] = [
      "#type" => "textfield",
      "#title" => $this->t("Tags"),
      "#default_value" => implode(", ", $this->entity->getTags()),
      "#description" => $this->t("Optional keywords to help authors find this node when searching. Comma-separated, e.g. input, text, user."),
    ];

    $form["basics"]["enabled"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Available to authors"),
      "#description" => $this->t("When unchecked, this node type is hidden from the workflow editor but its configuration is kept."),
      "#default_value" => $this->entity->isEnabled(),
    ];

    // === APPEARANCE SECTION ===
    $form["appearance"] = [
      "#type" => "fieldset",
      "#title" => $this->t("Appearance"),
    ];

    $form["appearance"]["description"] = [
      "#type" => "markup",
      "#markup" => '<p class="section-description">' .
      $this->t("Controls how the node looks on the workflow canvas.") .
      "</p>",
      "#weight" => -10,
    ];

    // Check if icon picker is enabled in FlowDrop settings.
    // Uses the configFactory() method inherited from FormBase.
    $iconPickerEnabled = (bool) $this->configFactory()
      ->get("flowdrop.settings")
      ->get("ui.icon_picker_enabled");

    // Attach icon picker library if enabled.
    if ($iconPickerEnabled) {
      $form["#attached"]["library"][] = "flowdrop/icon-picker-behavior";
    }

    // Appearance fields - build icon field with optional picker enhancement.
    $iconFieldAttributes = [];
    if ($iconPickerEnabled) {
      $iconFieldAttributes["class"] = ["flowdrop-icon-field"];
    }

    $form["appearance"]["icon"] = [
      "#type" => "textfield",
      "#title" => $this->t("Icon"),
      "#default_value" => $this->entity->getIcon(),
      "#description" => $iconPickerEnabled
        ? $this->t("The icon shown on the node. Use the picker to browse Material Design Icons, or type an identifier like mdi:cog.")
        : $this->t("The icon shown on the node, e.g. mdi:cog."),
      "#attributes" => $iconFieldAttributes,
    ];

    // Note: Color is inherited from the node's category.
    // No color field is needed here.
    // Visual type configuration.
    $visualTypes = $this->nodeTypeManager->getAvailableVisualTypes();

    // Gateway plugins should default to (and prefer) the "gateway" visual
    // type. Only treat the plugin as a gateway when that type is configured.
    $isGateway = isset($visualTypes["gateway"])
      && $this->isGatewayPlugin($selectedPlugin);
    $preferredType = $isGateway ? "gateway" : "default";

    // Supported visual types checkboxes.
    $supportedTypes = $this->entity->getSupportedVisualTypes();
    if (empty($supportedTypes)) {
      $supportedTypes = [$preferredType];
    }
    // On a new gateway, swap the untouched ["default"] for ["gateway"].
    // Never override an explicit choice on an existing node type.
    elseif ($isNewEntity && $isGateway && $supportedTypes === ["default"]) {
      $supportedTypes = ["gateway"];
    }
    $supportedTypesDefaults = array_combine($supportedTypes, $supportedTypes);

    $supportedTypesAttributes = [];
    if ($isGateway) {
      $supportedTypesAttributes["class"][] = "fd-gateway-visual-types";
    }

    $form["appearance"]["supported_visual_types"] = [
      "#type" => "checkboxes",
      "#title" => $this->t("Shapes this node can use"),
      "#description" => $this->t("The shapes available for this node on the canvas. If you tick more than one, authors can switch shape per node. Most nodes only need 'Default'."),
      "#options" => $visualTypes,
      "#default_value" => $supportedTypesDefaults,
      "#required" => TRUE,
      "#attributes" => $supportedTypesAttributes,
    ];

    // Live warning (toggled by JS) when a gateway also enables non-gateway
    // visual types, which may not render branches correctly.
    if ($isGateway) {
      $form["#attached"]["library"][] = "flowdrop_node_type/gateway_visual_type";
      $form["appearance"]["supported_visual_types_gateway_warning"] = [
        "#type" => "markup",
        "#markup" => '<div class="messages messages--warning" data-fd-gateway-visual-warning hidden>' .
        $this->t('This is a gateway node. Gateways are designed for the "Gateway" visual type; other selected types may not render branches correctly.') .
        "</div>",
      ];
    }

    // Gateways default to the gateway visual type on new node types; never
    // override an explicit choice on an existing one.
    $visualTypeDefault = $this->entity->getVisualType();
    if ($isNewEntity && $isGateway && $visualTypeDefault === "default") {
      $visualTypeDefault = "gateway";
    }

    $form["appearance"]["visual_type"] = [
      "#type" => "select",
      "#title" => $this->t("Default shape"),
      "#description" => $this->t("The shape a node starts with when an author adds it. Must be one of the shapes ticked above."),
      "#default_value" => $visualTypeDefault,
      "#options" => $visualTypes,
      "#required" => TRUE,
    ];

    // === ADVANCED SECTION ===
    // System-level metadata that site builders rarely need to touch, tucked
    // into a collapsed details element to reduce clutter.
    $form["advanced"] = [
      "#type" => "details",
      "#title" => $this->t("Advanced"),
      "#open" => FALSE,
      "#weight" => 50,
    ];

    // Plugin version display with mismatch warning.
    $currentPluginVersion = $this->getPluginVersion($selectedPlugin);
    $storedPluginVersion = $this->entity->getPluginVersion();

    $form["advanced"]["plugin_version_wrapper"] = [
      "#type" => "container",
      "#attributes" => ["class" => ["plugin-version-wrapper"]],
    ];

    $form["advanced"]["plugin_version_wrapper"]["plugin_version"] = [
      "#type" => "textfield",
      "#title" => $this->t("Plugin Version"),
      "#default_value" => $currentPluginVersion,
      "#size" => 10,
      "#disabled" => TRUE,
      "#description" => $this->t("Captured automatically from the selected plugin. You don't need to change this."),
    ];

    if (!$isNewEntity && !empty($storedPluginVersion) && !empty($currentPluginVersion) && $storedPluginVersion !== $currentPluginVersion) {
      $form["advanced"]["plugin_version_wrapper"]["version_mismatch_warning"] = [
        "#type" => "markup",
        "#markup" => '<div class="messages messages--warning">' .
        $this->t("Plugin version mismatch detected. Stored: @stored, Current: @current. Please review the configuration before saving.", [
          "@stored" => $storedPluginVersion,
          "@current" => $currentPluginVersion,
        ]) .
        "</div>",
      ];
    }

    // === PARAMETERS SECTION ===
    $form["parameters_section"] = $this->buildParametersTable($selectedPlugin);

    // === OUTPUTS SECTION ===
    $form["outputs_section"] = $this->buildOutputsTable($selectedPlugin);

    // === UNIFIED PORTS SECTION ===
    $form["unified_ports"] = $this->buildUnifiedPortsSection();

    // === TOOL EXPOSURE SECTION ===
    $form["tool_exposure"] = $this->buildToolExposureSection();
  }

  /**
   * Build unified ports configuration section.
   *
   * Unified ports allow passing all connectable inputs or all exposed
   * outputs as a single JSON object, which is useful for data pass-through
   * scenarios and tool integrations.
   *
   * @return array<string, mixed>
   *   Render array for the unified ports section.
   *
   * @see docs/development/reserved-names.md
   */
  protected function buildUnifiedPortsSection(): array {
    if (!$this->entity instanceof FlowDropNodeTypeInterface) {
      return [];
    }

    $section = [
      "#type" => "details",
      "#title" => $this->t("Combined ports"),
      "#open" => FALSE,
      "#weight" => 40,
    ];

    $section["description"] = [
      "#type" => "markup",
      "#markup" => '<p class="section-description">' .
      $this->t("Optional. Adds a single port that carries every value at once, instead of one port per input or output — handy for passing data straight through to another node. Leave these off unless you need it.") .
      "</p>",
    ];

    $section["unified_input_exposed"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Add a combined input port"),
      "#description" => $this->t("Lets a single connection deliver all of this node's inputs together. If both the combined port and an individual port are connected, the individual one wins."),
      "#default_value" => $this->entity->isUnifiedInputExposed(),
    ];

    $section["unified_output_exposed"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Add a combined output port"),
      "#description" => $this->t("Provides all of this node's outputs through one port, alongside the individual output ports."),
      "#default_value" => $this->entity->isUnifiedOutputExposed(),
    ];

    return $section;
  }

  /**
   * Build tool exposure configuration section.
   *
   * When a node type is exposed as a tool, instances render a "tool" output
   * port in the editor and can be wired as callable tools for tool-aware
   * consumers (agents), directly or through a ToolBox.
   *
   * @return array<string, mixed>
   *   Render array for the tool exposure section.
   *
   * @see docs/development/tool-aware-node-processors.md
   */
  protected function buildToolExposureSection(): array {
    if (!$this->entity instanceof FlowDropNodeTypeInterface) {
      return [];
    }

    $section = [
      "#type" => "details",
      "#title" => $this->t("Tool"),
      "#open" => FALSE,
      "#weight" => 41,
    ];

    $section["tool_exposed"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Available as Tool"),
      "#description" => $this->t("Adds a tool output port so agents can call this node as a tool with their own arguments. The tool must still be wired to a consumer on the canvas. Note: arguments supplied by the model take precedence over values configured on the node instance."),
      "#default_value" => $this->entity->isToolExposed(),
    ];

    return $section;
  }

  /**
   * {@inheritdoc}
   */
  protected function actions(array $form, FormStateInterface $form_state): array {
    $isNewEntity = $this->entity->isNew();
    $isConfigStep = $this->isConfigurationStep();

    $actions = [];

    // Step 1: Plugin Selection - show "Continue" button.
    if (!$isConfigStep) {
      $actions["next"] = [
        "#type" => "submit",
        "#value" => $this->t("Continue"),
        "#button_type" => "primary",
        "#submit" => ["::submitPluginSelection"],
        "#validate" => ["::validatePluginSelection"],
        // Auto-submit JS hides this and triggers it on selection; it stays
        // as a no-JS fallback.
        "#attributes" => ["class" => ["fd-plugin-continue"]],
      ];
    }
    // Step 2: Configuration - show "Back" and "Save" buttons.
    else {
      // Only show "Back" button for new entities.
      if ($isNewEntity) {
        $actions["back"] = [
          "#type" => "link",
          "#title" => $this->t("Back"),
          "#url" => Url::fromRoute("entity.flowdrop_node_type.add_form"),
          "#attributes" => ["class" => ["button"]],
          "#weight" => -10,
        ];
      }

      $actions["submit"] = [
        "#type" => "submit",
        "#value" => $this->t("Save"),
        "#button_type" => "primary",
        "#validate" => ["::validateForm"],
        "#submit" => ["::submitForm", "::save"],
      ];
    }

    // Add cancel link.
    $actions["cancel"] = [
      "#type" => "link",
      "#title" => $this->t("Cancel"),
      "#url" => Url::fromRoute("entity.flowdrop_node_type.collection"),
      "#attributes" => ["class" => ["button"]],
    ];

    return $actions;
  }

  /**
   * Validate plugin selection step.
   *
   * @param array<string, mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function validatePluginSelection(array &$form, FormStateInterface $form_state): void {
    $selectedPlugin = $form_state->getValue("executor_plugin");

    if (empty($selectedPlugin)) {
      // Set error on the plugin_section container for better visibility.
      $form_state->setErrorByName(
        "plugin_section",
        $this->t("Please select an executor plugin to continue.")
      );
    }
    else {
      // Validate that the selected plugin actually exists.
      $definition = $this->getPluginDefinition($selectedPlugin);
      if ($definition === NULL) {
        $form_state->setErrorByName(
          "plugin_section",
          $this->t("The selected plugin does not exist. Please select a valid plugin.")
        );
      }
    }
  }

  /**
   * Submit handler for plugin selection step.
   *
   * Redirects to the same form with plugin in URL query parameter.
   *
   * @param array<string, mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function submitPluginSelection(array &$form, FormStateInterface $form_state): void {
    $selectedPlugin = $form_state->getValue("executor_plugin");

    // Redirect to step 2 with plugin in URL.
    $form_state->setRedirect(
      "entity.flowdrop_node_type.add_form",
      [],
      ["query" => ["plugin" => $selectedPlugin]]
    );
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    parent::validateForm($form, $form_state);

    // Only validate configuration fields in step 2.
    if (!$this->isConfigurationStep()) {
      return;
    }

    // Validate machine name for new entities - check if ID already exists.
    if ($this->entity->isNew()) {
      $id = $form_state->getValue("id");
      if (!empty($id)) {
        $existing = FlowDropNodeType::load($id);
        if ($existing !== NULL) {
          $idElement = $form["basics"]["id"] ?? NULL;
          if ($idElement !== NULL) {
            $form_state->setError(
              $idElement,
              $this->t('The machine name "%id" is already in use. Please choose a different one.', ["%id" => $id])
            );
          }
          else {
            $form_state->setErrorByName(
              "id",
              $this->t('The machine name "%id" is already in use. Please choose a different one.', ["%id" => $id])
            );
          }
        }
      }
    }

    // Validate tags (only if form element exists).
    $tagsElement = $form["basics"]["tags_csv"] ?? NULL;
    $tagsCsv = $form_state->getValue("tags_csv");
    if ($tagsElement !== NULL && !empty($tagsCsv)) {
      $tags = array_map("trim", explode(",", $tagsCsv));
      foreach ($tags as $tag) {
        if (!empty($tag) && preg_match('/[^a-zA-Z0-9\-_]/', $tag)) {
          $form_state->setError(
            $tagsElement,
            $this->t("Invalid tag: @tag", ["@tag" => $tag])
          );
          break;
        }
      }
    }

    // Validate supported visual types and default visual type.
    $supportedTypesElement = $form["appearance"]["supported_visual_types"] ?? NULL;
    $visualTypeElement = $form["appearance"]["visual_type"] ?? NULL;

    if ($supportedTypesElement !== NULL) {
      $supportedTypes = $form_state->getValue("supported_visual_types") ?? [];
      $selectedSupportedTypes = array_filter($supportedTypes);

      if (empty($selectedSupportedTypes)) {
        $form_state->setError(
          $supportedTypesElement,
          $this->t("At least one supported visual type must be selected.")
        );
      }

      $visualType = $form_state->getValue("visual_type") ?? "default";
      if ($visualTypeElement !== NULL && !empty($selectedSupportedTypes) && !in_array($visualType, $selectedSupportedTypes, TRUE)) {
        $form_state->setError(
          $visualTypeElement,
          $this->t("The default visual type must be one of the supported visual types.")
        );
      }
    }

    // Warn about invalid parameter configurations.
    $parameters = $form_state->getValue("parameters") ?? [];
    foreach ($parameters as $paramName => $paramConfig) {
      $required = !empty($paramConfig["required"]);
      $configurable = !empty($paramConfig["configurable"]);
      $connectable = !empty($paramConfig["connectable"]);

      if ($required && !$configurable && !$connectable) {
        $this->messenger()->addWarning($this->t(
          'Parameter "@param" is required but has no source.',
          ["@param" => $paramName]
        ));
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    parent::submitForm($form, $form_state);

    if (!$this->entity instanceof FlowDropNodeTypeInterface) {
      return;
    }

    // Set basic fields.
    // Note: Color is not set here as it is inherited from the node's category.
    $this->entity->setDescription($form_state->getValue("description") ?? "");
    $this->entity->setCategory($form_state->getValue("category") ?? "processing");
    $this->entity->setIcon($form_state->getValue("icon") ?? "mdi:cog");
    $this->entity->setVisualType($form_state->getValue("visual_type") ?? "default");
    $this->entity->setEnabled((bool) $form_state->getValue("enabled"));

    // Set supported visual types (filter out unchecked values).
    $supportedTypes = $form_state->getValue("supported_visual_types") ?? [];
    $selectedSupportedTypes = array_values(array_filter($supportedTypes));
    if (empty($selectedSupportedTypes)) {
      $selectedSupportedTypes = ["default"];
    }
    $this->entity->setSupportedVisualTypes($selectedSupportedTypes);

    // Set executor plugin and capture its version automatically.
    $executorPlugin = $form_state->getValue("executor_plugin") ?? "";
    $this->entity->setExecutorPlugin($executorPlugin);
    $this->entity->setPluginVersion($this->getPluginVersion($executorPlugin));

    // Process parameters from table.
    $rawParameters = $form_state->getValue("parameters") ?? [];
    $selectedPlugin = $form_state->getValue("executor_plugin");
    $parameters = [];

    if ($selectedPlugin && !empty($rawParameters)) {
      $parameterSchema = $this->getPluginParameterSchema($selectedPlugin);
      $schemaProperties = $parameterSchema["properties"] ?? [];

      foreach ($rawParameters as $paramName => $paramConfig) {
        $schema = $schemaProperties[$paramName] ?? [];
        $type = $schema["type"] ?? "string";

        // Config-schema-only params (dynamicInputs, dynamicOutputs,
        // branches) are never connectable. Their connectable checkbox
        // is disabled in the form, so we force the value here since
        // disabled elements don't submit.
        $isConfigSchemaOnly = in_array($paramName, ReservedName::configSchemaOnlyParams(), TRUE);

        $parameters[$paramName] = [
          "configurable" => !empty($paramConfig["configurable"]),
          "connectable" => $isConfigSchemaOnly ? FALSE : !empty($paramConfig["connectable"]),
          "required" => !empty($paramConfig["required"]),
        ];

        // Process default value.
        $defaultValue = $paramConfig["default"] ?? NULL;

        if ($defaultValue !== NULL && $defaultValue !== "") {
          if (in_array($type, ["array", "object"])) {
            $decoded = json_decode($defaultValue, TRUE);
            if (json_last_error() === JSON_ERROR_NONE) {
              $parameters[$paramName]["default"] = $decoded;
            }
          }
          elseif ($type === "integer") {
            $parameters[$paramName]["default"] = (int) $defaultValue;
          }
          elseif ($type === "number") {
            $parameters[$paramName]["default"] = (float) $defaultValue;
          }
          elseif ($type === "boolean") {
            $parameters[$paramName]["default"] = (bool) $defaultValue;
          }
          else {
            $parameters[$paramName]["default"] = $defaultValue;
          }
        }
      }
    }

    $this->entity->setParameters($parameters);

    // Process outputs from table.
    $rawOutputs = $form_state->getValue("outputs") ?? [];
    $outputs = [];

    foreach ($rawOutputs as $outputName => $outputConfig) {
      $outputs[$outputName] = [
        "exposed" => !empty($outputConfig["exposed"]),
      ];
    }

    $this->entity->setOutputs($outputs);

    // Set unified port visibility.
    $this->entity->setUnifiedInputExposed(
      (bool) $form_state->getValue("unified_input_exposed")
    );
    $this->entity->setUnifiedOutputExposed(
      (bool) $form_state->getValue("unified_output_exposed")
    );

    // Set tool exposure.
    $this->entity->setToolExposed(
      (bool) $form_state->getValue("tool_exposed")
    );

    // Set tags.
    $tagsCsv = $form_state->getValue("tags_csv");
    if (!empty($tagsCsv)) {
      $tags = array_filter(array_map("trim", explode(",", $tagsCsv)));
      $this->entity->setTags($tags);
    }
    else {
      $this->entity->setTags([]);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function save(array $form, FormStateInterface $form_state): int {
    $result = parent::save($form, $form_state);

    $this->messenger()->addStatus(
      match ($result) {
        \SAVED_NEW => $this->t("Created %label.", ["%label" => $this->entity->label()]),
        default => $this->t("Updated %label.", ["%label" => $this->entity->label()]),
      }
    );

    $form_state->setRedirectUrl($this->entity->toUrl("collection"));
    return $result;
  }

}
