<?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\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.
 *
 * @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\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 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();

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

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

    // 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,
    ];

    // === 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 = (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["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)"),
    ];

    // 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["general"]["icon"] = [
      "#type" => "textfield",
      "#title" => $this->t("Icon"),
      "#default_value" => $this->entity->getIcon(),
      "#description" => $iconPickerEnabled
        ? $this->t("Use the icon picker to browse Material Design Icons, or enter an icon identifier manually (e.g., mdi:cog, mdi:account).")
        : $this->t("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();

    // 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);

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

  /**
   * 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("Unified Ports"),
      "#open" => TRUE,
    ];

    $section["description"] = [
      "#type" => "markup",
      "#markup" => '<p class="section-description">' .
      $this->t("Unified ports allow passing all data as a single JSON object. The 'input' and 'output' names are reserved system names.") .
      "</p>",
    ];

    $section["unified_input_exposed"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Show unified input port"),
      "#description" => $this->t("Adds an 'input' port that accepts all connectable parameters as a JSON object. When both the unified input and individual ports are connected, individual port values take precedence."),
      "#default_value" => $this->entity->isUnifiedInputExposed(),
    ];

    $section["unified_output_exposed"] = [
      "#type" => "checkbox",
      "#title" => $this->t("Show unified output port"),
      "#description" => $this->t("Adds an 'output' port that provides all exposed outputs as a JSON object. This coexists with individual output ports."),
      "#default_value" => $this->entity->isUnifiedOutputExposed(),
    ];

    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"],
      ];
    }
    // 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["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 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.
    // 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 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;
  }

}
