<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Form\Trait;

use Drupal\Component\Utility\Html;
use Drupal\flowdrop\Constants\ReservedName;

/**
 * Provides form section builders for the node type form.
 *
 * Extracts the render array construction methods for parameters table,
 * outputs table, step indicator, and type display formatting.
 *
 * Requirements for using this trait:
 * - The class must use NodeTypePluginHelperTrait (for schema methods)
 * - The class must have an $entity property of type FlowDropNodeTypeInterface
 * - The class must implement t() for translation
 *
 * @internal This trait is not part of the public API and may change without notice.
 */
trait NodeTypeFormSectionsTrait {

  /**
   * 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 parameters configuration table.
   *
   * Internal parameters (names starting with "__") are filtered out because
   * they are system-managed and should not be configured by users.
   *
   * @param string $pluginId
   *   The plugin ID.
   *
   * @return array<string, mixed>
   *   Render array for the parameters table.
   *
   * @see docs/development/internal-parameters.md
   */
  protected function buildParametersTable(string $pluginId): array {
    $parameterSchema = $this->getPluginParameterSchema($pluginId);
    $entityParameters = $this->entity->getParameters();
    $allProperties = $parameterSchema["properties"] ?? [];

    // Filter out internal parameters (those starting with "__").
    // Internal params like __iterator__, __state__, __messages__, etc.
    // are auto-injected by orchestrator and should not be user-configurable.
    $properties = array_filter(
      $allProperties,
      fn(string $paramName): bool => !str_starts_with($paramName, "__"),
      ARRAY_FILTER_USE_KEY
    );

    $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>",
      "#weight" => -2,
    ];

    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. Each setting names where a value can come
    // from; the resolution note explains the order they win in at runtime.
    $section["legend"] = [
      "#type" => "markup",
      "#markup" => '<div class="table-legend">' .
      '<span><strong>' . $this->t("Config:") . '</strong> ' . $this->t("tick to expose the corresponding parameter in the workflow config panel, so an author can set a value (per workflow)") . '</span>' .
      '<span><strong>' . $this->t("Show Port:") . '</strong> ' . $this->t("tick to expose the corresponding parameter as an input port, so a connected node can supply a value fresh each run (per run)") . '</span>' .
      '<span><strong>' . $this->t("Req'd:") . '</strong> ' . $this->t("tick to require a value when the node runs (from any source above)") . '</span>' .
      '<span><strong>' . $this->t("Default:") . '</strong> ' . $this->t("the fallback value used everywhere this node appears, when nothing else supplies one (global)") . '</span>' .
      "</div>" .
      '<p class="table-legend__resolution">' .
      $this->t("Together these are the node's value contract. When the node runs, FlowDrop uses the first value it finds, in this order: a connected port (per run) wins first, then the value saved on the workflow (per workflow), and finally the Default (global). So the Default is only used as a fallback when neither a connection nor a workflow value is provided.") .
      "</p>",
      "#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;

      // Check for readOnly parameters (JSON Schema standard).
      // readOnly parameters show the default value but cannot be edited.
      $isReadOnlyParameter = ($paramSchema["readOnly"] ?? FALSE) === TRUE;

      // Config-schema-only parameters (dynamicInputs, dynamicOutputs,
      // branches) are always routed to configSchema by the API layer.
      // Lock their checkboxes to reflect this — configurable is forced on,
      // connectable is forced off.
      $isConfigSchemaOnly = in_array($paramName, ReservedName::configSchemaOnlyParams(), 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>' . Html::escape($paramName) . '</strong> <code>' . Html::escape($typeDisplay) . "</code>";
      if ($isConfigSchemaOnly) {
        $nameMarkup .= ' <em class="form-item--disabled">(' . $this->t("config only") . ")</em>";
      }
      elseif ($isHiddenParameter) {
        $nameMarkup .= ' <em class="form-item--disabled">(' . $this->t("auto-injected") . ")</em>";
      }
      elseif ($isReadOnlyParameter) {
        $nameMarkup .= ' <em class="form-item--disabled">(' . $this->t("read-only") . ")</em>";
      }
      if (!empty($paramSchema["title"]) && $paramSchema["title"] !== $paramName) {
        $nameMarkup .= '<br><small>' . Html::escape($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" => $isConfigSchemaOnly ? FALSE : ($entityConfig["connectable"] ?? $flowdropDefaults["connectable"] ?? FALSE),
        "#disabled" => $isHiddenParameter || $isConfigSchemaOnly,
      ];

      $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
      );
      // Disable default field for hidden and readOnly parameters.
      $section["parameters"][$paramName]["default"]["#disabled"] = $isHiddenParameter || $isReadOnlyParameter;
    }

    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";
        // Handle arrays, stdClass objects, other objects by encoding to JSON.
        if (is_array($defaultValue) || is_object($defaultValue)) {
          $field["#default_value"] = json_encode($defaultValue);
        }
        else {
          $field["#default_value"] = is_string($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>' . Html::escape($outputName) . '</strong> <code>' . Html::escape($outputTypeDisplay) . "</code>" .
        (!empty($outputDef["title"]) && $outputDef["title"] !== $outputName
            ? '<br><small>' . Html::escape($outputDef["title"]) . "</small>"
            : ""),
      ];

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

    return $section;
  }

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

}
