<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Form;

use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_type\Entity\FlowDropNodeType;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManager;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * FlowDrop Node Type form with tabular parameter configuration.
 *
 * @see docs/development/unified-parameter-system-spec.md
 */
final class FlowDropNodeTypeForm extends EntityForm {

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

  /**
   * The node type manager service.
   *
   * @var \Drupal\flowdrop_node_type\Service\FlowDropNodeTypeManager
   */
  protected FlowDropNodeTypeManager $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;
  }

  /**
   * {@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";

    // Get available plugins.
    $availablePlugins = $this->getAvailablePlugins();

    // Get selected plugin (from AJAX or entity).
    $selectedPlugin = $form_state->getValue("executor_plugin") ?: $this->entity->getExecutorPlugin();

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

    // Determine if we should autofill from plugin
    // (only for new entities or when plugin changes).
    $isNewEntity = $this->entity->isNew();
    $triggeredBy = $form_state->getTriggeringElement();

    // Check if the executor_plugin dropdown triggered the AJAX.
    // Use #array_parents which contains the form tree path
    // (e.g., ['plugin_section', 'executor_plugin']).
    $triggerArrayParents = $triggeredBy["#array_parents"] ?? [];
    $isPluginChange = $triggeredBy !== NULL && in_array("executor_plugin", $triggerArrayParents, TRUE);

    // When plugin changes, clear the user input for autofill fields.
    // This forces the form to use #default_value instead of the old user input.
    if ($isPluginChange) {
      $userInput = $form_state->getUserInput();
      unset($userInput["label"], $userInput["description"]);

      // Only clear the ID for new entities
      // (existing entities have disabled machine name).
      if ($isNewEntity) {
        unset($userInput["id"]);
      }

      $form_state->setUserInput($userInput);
    }

    // === PLUGIN SELECTION SECTION (TOP) ===
    $form["plugin_section"] = [
      "#type" => "fieldset",
      "#title" => $this->t("Executor Plugin"),
      "#weight" => -10,
    ];

    $form["plugin_section"]["executor_plugin"] = [
      "#type" => "select",
      "#title" => $this->t("Select Plugin"),
      "#default_value" => $selectedPlugin,
      "#options" => $availablePlugins,
      "#required" => TRUE,
      "#empty_option" => $this->t("- Select a plugin -"),
      "#ajax" => [
        "callback" => "::updateFormFromPlugin",
        "wrapper" => "flowdrop-node-type-form-wrapper",
      ],
      "#description" => $this->t("Select a plugin to auto-populate form fields and configure parameters."),
    ];

    // === MAIN FORM WRAPPER (for AJAX updates) ===
    $form["form_wrapper"] = [
      "#type" => "container",
      "#prefix" => '<div id="flowdrop-node-type-form-wrapper">',
      "#suffix" => "</div>",
      "#weight" => 0,
    ];

    if (!$selectedPlugin) {
      $form["form_wrapper"]["no_plugin"] = [
        "#type" => "markup",
        "#markup" => '<div class="messages messages--warning">' .
        $this->t("Please select an executor plugin to continue.") .
        "</div>",
      ];
      return $form;
    }

    // === GENERAL SECTION ===
    $form["form_wrapper"]["general"] = [
      "#type" => "fieldset",
      "#title" => $this->t("General"),
    ];

    // Auto-fill from plugin when plugin changes
    // (forcefully overwrite all fields).
    if ($isPluginChange && $pluginDefinition !== NULL) {
      $pluginLabel = $pluginDefinition["label"] ?? "";
      $labelDefault = $pluginLabel instanceof \Stringable ? $pluginLabel->render() : (string) $pluginLabel;
      $descriptionDefault = $pluginDefinition["description"] ?? "";
      // Auto-fill machine name from plugin ID for new entities.
      $idDefault = $isNewEntity ? $selectedPlugin : $this->entity->id();
    }
    else {
      // Use existing values (from form state or entity).
      $labelDefault = $form_state->getValue("label") ?? $this->entity->label();
      $descriptionDefault = $form_state->getValue("description") ?? $this->entity->getDescription();
      $idDefault = $this->entity->id();

      // For new entities with empty fields, auto-fill from plugin.
      if ($isNewEntity && $pluginDefinition !== NULL) {
        if (empty($labelDefault)) {
          $pluginLabel = $pluginDefinition["label"] ?? "";
          $labelDefault = $pluginLabel instanceof \Stringable ? $pluginLabel->render() : (string) $pluginLabel;
        }
        if (empty($descriptionDefault)) {
          $descriptionDefault = $pluginDefinition["description"] ?? "";
        }
        if (empty($idDefault)) {
          $idDefault = $selectedPlugin;
        }
      }
    }

    $form["form_wrapper"]["general"]["label"] = [
      "#type" => "textfield",
      "#title" => $this->t("Label"),
      "#maxlength" => 255,
      "#default_value" => $labelDefault,
      "#required" => TRUE,
    ];

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

    $form["form_wrapper"]["general"]["description"] = [
      "#type" => "textarea",
      "#title" => $this->t("Description"),
      "#default_value" => $descriptionDefault,
      "#rows" => 2,
    ];

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

    $form["form_wrapper"]["general"]["category"] = [
      "#type" => "select",
      "#title" => $this->t("Category"),
      "#default_value" => $this->entity->getCategory(),
      "#options" => $categories,
      "#required" => TRUE,
      "#empty_option" => $this->t("- Select -"),
    ];

    $form["form_wrapper"]["general"]["tags_csv"] = [
      "#type" => "textfield",
      "#title" => $this->t("Tags"),
      "#default_value" => implode(", ", $this->entity->getTags()),
      "#description" => $this->t("Comma-separated (e.g., input, text, user)"),
    ];

    // Appearance fields in same section.
    $form["form_wrapper"]["general"]["icon"] = [
      "#type" => "textfield",
      "#title" => $this->t("Icon"),
      "#default_value" => $this->entity->getIcon(),
      "#description" => $this->t("e.g., mdi:cog"),
    ];

    $form["form_wrapper"]["general"]["color"] = [
      "#type" => "color",
      "#title" => $this->t("Color"),
      "#default_value" => $this->entity->getColor(),
    ];

    // Visual type configuration.
    $visualTypes = $this->nodeTypeManager->getAvailableVisualTypes();

    // Supported visual types checkboxes.
    $supportedTypes = $this->entity->getSupportedVisualTypes();
    // Ensure at least "default" is selected for new entities.
    if (empty($supportedTypes)) {
      $supportedTypes = ["default"];
    }
    // Convert to associative array for checkboxes default_value.
    $supportedTypesDefaults = array_combine($supportedTypes, $supportedTypes);

    $form["form_wrapper"]["general"]["supported_visual_types"] = [
      "#type" => "checkboxes",
      "#title" => $this->t("Supported Visual Types"),
      "#description" => $this->t("Select the visual types this node can use. Users can switch between these types per node instance in the workflow editor."),
      "#options" => $visualTypes,
      "#default_value" => $supportedTypesDefaults,
      "#required" => TRUE,
    ];

    // Default visual type selection (filtered by supported types).
    $form["form_wrapper"]["general"]["visual_type"] = [
      "#type" => "select",
      "#title" => $this->t("Default Visual Type"),
      "#description" => $this->t("The default visual type when this node is added to a workflow. Must be one of the supported types above."),
      "#default_value" => $this->entity->getVisualType(),
      "#options" => $visualTypes,
      "#required" => TRUE,
    ];

    // Get the current plugin version and stored plugin version.
    $currentPluginVersion = $this->getPluginVersion($selectedPlugin);
    $storedPluginVersion = $this->entity->getPluginVersion();

    // Build the plugin version display with mismatch warning if applicable.
    $form["form_wrapper"]["general"]["plugin_version_wrapper"] = [
      "#type" => "container",
      "#attributes" => ["class" => ["plugin-version-wrapper"]],
    ];

    $form["form_wrapper"]["general"]["plugin_version_wrapper"]["plugin_version"] = [
      "#type" => "textfield",
      "#title" => $this->t("Plugin Version"),
      "#default_value" => $currentPluginVersion,
      "#size" => 10,
      "#disabled" => TRUE,
      "#description" => $this->t("Version is automatically captured from the selected executor plugin."),
    ];

    // Show version mismatch warning if:
    // 1. Entity is not new (has been saved before).
    // 2. There's a stored version.
    // 3. The stored version differs from current plugin version.
    if (!$isNewEntity && !empty($storedPluginVersion) && !empty($currentPluginVersion) && $storedPluginVersion !== $currentPluginVersion) {
      $form["form_wrapper"]["general"]["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>",
      ];
    }

    $form["form_wrapper"]["general"]["enabled"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Enabled"),
      "#default_value" => $this->entity->isEnabled(),
    ];

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

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

    return $form;
  }

  /**
   * Build parameters configuration table.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return array<string, mixed>
   *   Render array for the parameters table.
   */
  protected function buildParametersTable(string $pluginId): array {
    $parameterSchema = $this->getPluginParameterSchema($pluginId);
    $entityParameters = $this->entity->getParameters();
    $properties = $parameterSchema["properties"] ?? [];

    $section = [
      "#type" => "fieldset",
      "#title" => $this->t("Parameters (@count)", ["@count" => count($properties)]),
    ];

    $section["description"] = [
      "#type" => "markup",
      "#markup" => '<p class="section-description">' .
      $this->t("Parameters define the inputs this node accepts. Configure how each parameter can receive values:") .
      "</p>",
    ];

    if (empty($properties)) {
      $section["empty"] = [
        "#markup" => '<em>' . $this->t("This plugin has no parameters.") . "</em>",
      ];
      return $section;
    }

    // Build table header with descriptions.
    $section["parameters"] = [
      "#type" => "table",
      '#colgroups' => [
        [
          [
            'span' => 1,
          ],
          [
            'span' => 3,
            'class' => 'flowdrop--narrow-col',
          ],
          [
            'span' => 1,
            'class' => 'flowdrop--form-col',
          ],
        ],
      ],
      "#header" => [
        $this->t("Parameter"),
        [
          "data" => $this->t("Config"),
          "title" => $this->t("Config: Show in the node's configuration panel. Users can set a static value."),
        ],
        [
          "data" => $this->t("Show Port"),
          "title" => $this->t("Show Port: Show as an input port. Users can connect other nodes to provide dynamic values."),
        ],
        [
          "data" => $this->t("Req'd"),
          "title" => $this->t("Req'd: A value must be provided, either via configuration or connection."),
        ],
        $this->t("Default"),
      ],
      "#empty" => $this->t("No parameters defined."),
      "#attributes" => ["class" => ["parameters-table"]],
    ];

    // Add legend below header.
    $section["legend"] = [
      "#type" => "markup",
      "#markup" => '<div class="table-legend">' .
      '<span><strong>' . $this->t("Config:") . '</strong> ' . $this->t("Shows in config panel (static value)") . '</span>' .
      '<span><strong>' . $this->t("Show Port:") . '</strong> ' . $this->t("Shows as input port (dynamic value)") . '</span>' .
      '<span><strong>' . $this->t("Req'd:") . '</strong> ' . $this->t("Must have a value at runtime") . '</span>' .
      "</div>",
      "#weight" => -1,
    ];

    foreach ($properties as $paramName => $paramSchema) {
      $entityConfig = $entityParameters[$paramName] ?? [];
      $flowdropDefaults = $paramSchema["flowdrop"] ?? [];
      $format = $paramSchema["format"] ?? NULL;

      // Check for hidden parameters: either format='hidden' or hidden=TRUE.
      // Hidden parameters are automatically injected by the runtime and
      // should be shown as disabled (greyed out) in the form.
      $isHiddenParameter = $format === "hidden" || ($paramSchema["hidden"] ?? FALSE) === TRUE;

      // Type display.
      $typeDisplay = $paramSchema["type"] ?? "mixed";
      if (isset($paramSchema["enum"])) {
        $typeDisplay .= " (enum)";
      }

      // Combined name and type in first column.
      // Add visual indicator for hidden parameters.
      $nameMarkup = '<strong>' . $paramName . '</strong> <code>' . $typeDisplay . "</code>";
      if ($isHiddenParameter) {
        $nameMarkup .= ' <em class="form-item--disabled">(' . $this->t("auto-injected") . ")</em>";
      }
      if (!empty($paramSchema["title"]) && $paramSchema["title"] !== $paramName) {
        $nameMarkup .= '<br><small>' . $paramSchema["title"] . "</small>";
      }

      $section["parameters"][$paramName]["name"] = [
        "#type" => "item",
        "#markup" => $nameMarkup,
      ];

      $section["parameters"][$paramName]["configurable"] = [
        "#type" => "checkbox",
        "#title" => "",
        "#title_display" => "invisible",
        "#default_value" => $entityConfig["configurable"] ?? $flowdropDefaults["configurable"] ?? FALSE,
        "#disabled" => $isHiddenParameter,
      ];

      $section["parameters"][$paramName]["connectable"] = [
        "#type" => "checkbox",
        "#title" => "",
        "#title_display" => "invisible",
        "#default_value" => $entityConfig["connectable"] ?? $flowdropDefaults["connectable"] ?? FALSE,
        "#disabled" => $isHiddenParameter,
      ];

      $section["parameters"][$paramName]["required"] = [
        "#type" => "checkbox",
        "#title" => "",
        "#title_display" => "invisible",
        "#default_value" => $entityConfig["required"] ?? $flowdropDefaults["required"] ?? FALSE,
        "#disabled" => $isHiddenParameter,
      ];

      // Default value field.
      $section["parameters"][$paramName]["default"] = $this->buildCompactDefaultField(
        $paramName,
        $paramSchema,
        $entityConfig["default"] ?? $paramSchema["default"] ?? NULL
      );
      $section["parameters"][$paramName]["default"]["#disabled"] = $isHiddenParameter;
    }

    return $section;
  }

  /**
   * Build a compact default value field.
   *
   * @param string $paramName
   *   The parameter name.
   * @param array<string, mixed> $paramSchema
   *   The parameter schema.
   * @param mixed $defaultValue
   *   The current default value.
   *
   * @return array<string, mixed>
   *   Render array for the default value field.
   */
  protected function buildCompactDefaultField(string $paramName, array $paramSchema, mixed $defaultValue): array {
    $type = $paramSchema["type"] ?? "string";

    $field = [
      "#title" => "",
      "#title_display" => "invisible",
    ];

    switch ($type) {
      case "trigger":
        break;

      case "boolean":
        $field["#type"] = "checkbox";
        $field["#default_value"] = (bool) $defaultValue;
        break;

      case "integer":
      case "number":
        $field["#type"] = "number";
        $field["#default_value"] = $defaultValue;
        $field["#size"] = 8;
        $field["#step"] = $type === "integer" ? 1 : "any";
        if (isset($paramSchema["minimum"])) {
          $field["#min"] = $paramSchema["minimum"];
        }
        if (isset($paramSchema["maximum"])) {
          $field["#max"] = $paramSchema["maximum"];
        }
        break;

      case "string":
        if (isset($paramSchema["enum"])) {
          $field["#type"] = "select";
          $field["#options"] = array_combine($paramSchema["enum"], $paramSchema["enum"]);
          $field["#default_value"] = $defaultValue;
          $field["#empty_option"] = "";
        }
        elseif (isset($paramSchema["format"]) && $paramSchema["format"] === "multiline") {
          $field["#type"] = "textarea";
          $field["#default_value"] = $defaultValue;
        }
        elseif (isset($paramSchema["format"]) && $paramSchema["format"] === "markdown") {
          $field["#type"] = "textarea";
          $field["#default_value"] = $defaultValue;
        }
        else {
          $field["#type"] = "textfield";
          $field["#default_value"] = $defaultValue;
          $field["#size"] = 20;
        }
        break;

      case "array":
      case "object":
        $field["#type"] = "textarea";
        $field["#default_value"] = is_array($defaultValue) ? json_encode($defaultValue) : $defaultValue;
        $field["#size"] = 20;
        $field["#placeholder"] = $type === "array" ? "[]" : "{}";
        break;

      default:
        $field["#type"] = "textarea";
        $field["#default_value"] = is_scalar($defaultValue) ? $defaultValue : "";
        $field["#size"] = 20;
        break;
    }

    return $field;
  }

  /**
   * Build outputs configuration table.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return array<string, mixed>
   *   Render array for the outputs table.
   */
  protected function buildOutputsTable(string $pluginId): array {
    $outputSchema = $this->getPluginOutputSchema($pluginId);
    $entityOutputs = $this->entity->getOutputs();
    $properties = $outputSchema["properties"] ?? [];

    $section = [
      "#type" => "fieldset",
      "#title" => $this->t("Outputs (@count)", ["@count" => count($properties)]),
    ];

    $section["description"] = [
      "#type" => "markup",
      "#markup" => '<p class="section-description">' .
      $this->t("Outputs define the data this node produces. Control which outputs appear as connection ports:") .
      "</p>",
    ];

    if (empty($properties)) {
      $section["empty"] = [
        "#markup" => '<em>' . $this->t("This plugin has no outputs.") . "</em>",
      ];
      return $section;
    }

    $section["legend"] = [
      "#type" => "markup",
      "#markup" => '<div class="table-legend">' .
      '<span><strong>' . $this->t("Show Port:") . '</strong> ' . $this->t("Shows as output port that can connect to other nodes") . '</span>' .
      "</div>",
    ];

    $section["outputs"] = [
      "#type" => "table",
      "#header" => [
        $this->t("Output"),
        [
          "data" => $this->t("Show Port"),
          "title" => $this->t("Show as an output port. When unchecked, this output is hidden from the workflow editor."),
        ],
      ],
      "#empty" => $this->t("No outputs defined."),
      "#attributes" => ["class" => ["outputs-table"]],
    ];

    foreach ($properties as $outputName => $outputDef) {
      $entityConfig = $entityOutputs[$outputName] ?? [];

      // Combined name and type in first column.
      $section["outputs"][$outputName]["name"] = [
        "#type" => "item",
        "#markup" => '<strong>' . $outputName . '</strong> <code>' . ($outputDef["type"] ?? "mixed") . "</code>" .
        (!empty($outputDef["title"]) && $outputDef["title"] !== $outputName
            ? '<br><small>' . $outputDef["title"] . "</small>"
            : ""),
      ];

      $section["outputs"][$outputName]["exposed"] = [
        "#type" => "checkbox",
        "#title" => "",
        "#title_display" => "invisible",
        "#default_value" => $entityConfig["exposed"] ?? TRUE,
      ];
    }

    return $section;
  }

  /**
   * Get available FlowDropNode plugins.
   *
   * @return array<string, string>
   *   Array of plugin IDs mapped to their labels.
   */
  protected function getAvailablePlugins(): array {
    $plugins = [];

    try {
      $definitions = $this->nodeProcessorPluginManager->getDefinitions();

      foreach ($definitions as $pluginId => $definition) {
        $label = $definition["label"];
        $plugins[$pluginId] = $label instanceof \Stringable ? $label->render() : (string) $label;
      }

      asort($plugins);
    }
    catch (\Exception $e) {
      $this->messenger()->addError($this->t("Error loading plugins: @error", ["@error" => $e->getMessage()]));
    }

    return $plugins;
  }

  /**
   * Get the parameter schema from a plugin.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return array<string, mixed>
   *   The plugin's parameter schema.
   */
  protected function getPluginParameterSchema(string $pluginId): array {
    try {
      $plugin = $this->nodeProcessorPluginManager->createInstance($pluginId);
      return $plugin->getParameterSchema();
    }
    catch (\Exception $e) {
      $this->messenger()->addError($this->t("Error loading parameter schema: @error", ["@error" => $e->getMessage()]));
      return [];
    }
  }

  /**
   * Get the output schema from a plugin.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return array<string, mixed>
   *   The plugin's output schema.
   */
  protected function getPluginOutputSchema(string $pluginId): array {
    try {
      $plugin = $this->nodeProcessorPluginManager->createInstance($pluginId);
      return $plugin->getOutputSchema();
    }
    catch (\Exception $e) {
      $this->messenger()->addError($this->t("Error loading output schema: @error", ["@error" => $e->getMessage()]));
      return [];
    }
  }

  /**
   * Get the full plugin definition.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return array<string, mixed>|null
   *   The plugin definition, or NULL if not found.
   */
  protected function getPluginDefinition(string $pluginId): ?array {
    if (empty($pluginId)) {
      return NULL;
    }

    try {
      $definition = $this->nodeProcessorPluginManager->getDefinition($pluginId, FALSE);
      return is_array($definition) ? $definition : NULL;
    }
    catch (\Exception $e) {
      return NULL;
    }
  }

  /**
   * Get the version of a plugin from its definition.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return string
   *   The plugin version, or empty string if not found.
   */
  protected function getPluginVersion(string $pluginId): string {
    $definition = $this->getPluginDefinition($pluginId);
    if ($definition !== NULL && isset($definition["version"])) {
      return (string) $definition["version"];
    }
    return "";
  }

  /**
   * AJAX callback to update the form when plugin selection changes.
   *
   * @param array<string, mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   The AJAX response.
   */
  public function updateFormFromPlugin(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand(
      "#flowdrop-node-type-form-wrapper",
      $form["form_wrapper"]
    ));
    return $response;
  }

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

    // Validate tags (only if form element exists).
    $tagsElement = $form["form_wrapper"]["general"]["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 color (only if form element exists).
    $colorElement = $form["form_wrapper"]["general"]["color"] ?? NULL;
    $color = $form_state->getValue("color");
    if ($colorElement !== NULL && !empty($color) && !preg_match('/^#[0-9a-fA-F]{6}$/', $color)) {
      $form_state->setError($colorElement, $this->t("Invalid hex color."));
    }

    // Validate supported visual types and default visual type.
    // Only validate if the form elements exist (plugin must be selected).
    $supportedTypesElement = $form["form_wrapper"]["general"]["supported_visual_types"] ?? NULL;
    $visualTypeElement = $form["form_wrapper"]["general"]["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.
    $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->setColor($form_state->getValue("color") ?? "#007cba");
    $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";

        $parameters[$paramName] = [
          "configurable" => !empty($paramConfig["configurable"]),
          "connectable" => !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 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;
  }

}
