<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Form;

use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
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 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.
 *
 * @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;
  }

  /**
   * 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;
  }

  /**
   * 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 the step indicator markup.
   *
   * @param bool $isConfigStep
   *   Whether we're in the configuration step.
   *
   * @return string
   *   The HTML markup for the step indicator.
   */
  protected function buildStepIndicator(bool $isConfigStep): string {
    $step1Class = $isConfigStep ? "step-item step-complete" : "step-item step-current";
    $step2Class = $isConfigStep ? "step-item step-current" : "step-item";

    return '<div class="form-step-indicator">' .
      '<span class="' . $step1Class . '">1. ' . $this->t("Select Plugin") . '</span>' .
      ' <span class="step-separator">&rarr;</span> ' .
      '<span class="' . $step2Class . '">2. ' . $this->t("Configure") . '</span>' .
      "</div>";
  }

  /**
   * Build Step 1: Plugin Selection.
   *
   * @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 {
    $availablePlugins = $this->getAvailablePlugins();

    $form["plugin_section"] = [
      "#type" => "fieldset",
      "#title" => $this->t("Step 1: Select Executor Plugin"),
    ];

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

    $form["plugin_section"]["executor_plugin"] = [
      "#type" => "select",
      "#title" => $this->t("Executor Plugin"),
      "#default_value" => "",
      "#options" => $availablePlugins,
      "#required" => TRUE,
      "#empty_option" => $this->t("- Select a plugin -"),
      "#description" => $this->t("Choose a plugin to see its available parameters and outputs in the next step."),
    ];
  }

  /**
   * 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 = $pluginLabel instanceof \Stringable ? $pluginLabel->render() : (string) $pluginLabel;

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

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

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

    // Determine default values for new entities (auto-fill from plugin).
    if ($isNewEntity && $pluginDefinition !== NULL) {
      $labelDefault = $pluginLabel instanceof \Stringable ? $pluginLabel->render() : (string) $pluginLabel;
      $descriptionDefault = $pluginDefinition["description"] ?? "";
      $idDefault = $selectedPlugin;
    }
    else {
      $labelDefault = $this->entity->label();
      $descriptionDefault = $this->entity->getDescription();
      $idDefault = $this->entity->id();
    }

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

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

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

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

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

    $form["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.
    $form["general"]["icon"] = [
      "#type" => "textfield",
      "#title" => $this->t("Icon"),
      "#default_value" => $this->entity->getIcon(),
      "#description" => $this->t("e.g., mdi:cog"),
    ];

    $form["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();
    if (empty($supportedTypes)) {
      $supportedTypes = ["default"];
    }
    $supportedTypesDefaults = array_combine($supportedTypes, $supportedTypes);

    $form["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,
    ];

    $form["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,
    ];

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

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

    $form["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."),
    ];

    if (!$isNewEntity && !empty($storedPluginVersion) && !empty($currentPluginVersion) && $storedPluginVersion !== $currentPluginVersion) {
      $form["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["general"]["enabled"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Enabled"),
      "#default_value" => $this->entity->isEnabled(),
    ];

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

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

  /**
   * 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.
      $isHiddenParameter = $format === "hidden" || ($paramSchema["hidden"] ?? FALSE) === TRUE;

      // Type display - handle array types (e.g., ["string", "null"] for nullable).
      $typeDisplay = $this->formatTypeDisplay($paramSchema["type"] ?? "mixed");
      if (isset($paramSchema["enum"])) {
        $typeDisplay .= " (enum)";
      }

      // Combined name and type in first column.
      $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.
      // Handle array types (e.g., ["string", "null"] for nullable).
      $outputTypeDisplay = $this->formatTypeDisplay($outputDef["type"] ?? "mixed");
      $section["outputs"][$outputName]["name"] = [
        "#type" => "item",
        "#markup" => '<strong>' . $outputName . '</strong> <code>' . $outputTypeDisplay . "</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 "";
  }

  /**
   * Format a JSON Schema type for display.
   *
   * Handles array types like ["string", "null"] for nullable fields.
   *
   * @param mixed $type
   *   The type value from JSON Schema (string or array).
   *
   * @return string
   *   A formatted string representation of the type.
   */
  protected function formatTypeDisplay(mixed $type): string {
    if (is_array($type)) {
      // Filter out "null" and join remaining types.
      $types = array_filter($type, fn($t) => $t !== "null");
      $formatted = implode("|", $types);
      // Add nullable indicator if "null" was in the array.
      if (in_array("null", $type, TRUE)) {
        $formatted .= "?";
      }
      return $formatted ?: "mixed";
    }

    return is_string($type) ? $type : "mixed";
  }

  /**
   * {@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"],
      ];
    }
    // 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)) {
      $form_state->setErrorByName(
        "executor_plugin",
        $this->t("Please select an executor plugin to continue.")
      );
    }
  }

  /**
   * 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["general"]["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["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["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.
    $supportedTypesElement = $form["general"]["supported_visual_types"] ?? NULL;
    $visualTypeElement = $form["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;
  }

}
