<?php

declare(strict_types=1);

namespace Drupal\flowdrop_node_type\Controller\Api;

use Drupal\Core\Cache\CacheableJsonResponse;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Controller\ControllerBase;
use Drupal\flowdrop\Constants\ReservedName;
use Drupal\flowdrop\Controller\Api\ApiResponseTrait;
use Drupal\flowdrop\DTO\NodeMetadata\NodeMetadata;
use Drupal\flowdrop\DTO\NodeMetadata\NodeMetadataBuilder;
use Drupal\flowdrop\DTO\NodeMetadata\NodePort;
use Drupal\flowdrop\Plugin\FlowDropNodeProcessor\ConfigEditProviderInterface;
use Drupal\flowdrop\Service\FlowDropNodeProcessorPluginManager;
use Drupal\flowdrop_node_type\FlowDropNodeTypeInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

/**
 * Controller for node type API endpoints.
 *
 * This controller uses the Unified Parameter System to derive inputSchema
 * and configSchema from the plugin's parameterSchema combined with the
 * config entity's parameter configuration.
 *
 * API response format:
 * - inputs: derived from parameters with connectable=TRUE
 * - configSchema: derived from parameters with configurable=TRUE
 * - config: default values from entity and plugin schema
 *
 * @internal This class is not part of the public API and may change without notice.
 *
 * @see docs/development/flowdrop-node-processor.md
 */
final class NodesController extends ControllerBase {

  use ApiResponseTrait;

  public function __construct(
    protected readonly FlowDropNodeProcessorPluginManager $pluginManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get("flowdrop.node_processor_plugin_manager")
    );
  }

  /**
   * Get all node types.
   *
   * Returns node types sorted alphabetically by category name in ascending
   * order (A-Z), then by node name within each category (also A-Z).
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with all node types.
   */
  public function getNodes(): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);
    // Add list cache tag - invalidated when any node type is added/removed.
    $cacheMetadata->addCacheTags(['config:flowdrop_node_type_list']);

    try {
      $storage = $this->entityTypeManager()->getStorage("flowdrop_node_type");
      $entities = $storage->loadMultiple();

      $nodes = [];
      foreach ($entities as $entity) {
        if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
          // Add cache tag for each individual entity.
          $cacheMetadata->addCacheableDependency($entity);

          $nodeMetadata = $this->buildNodeData($entity, $cacheMetadata);
          if ($nodeMetadata !== NULL) {
            $nodes[] = $nodeMetadata->toArray();
          }
        }
      }

      // Sort nodes alphabetically by category name (ascending A-Z),
      // then by node name within each category.
      usort($nodes, static function (array $a, array $b): int {
        $categoryA = $a["category"] ?? "";
        $categoryB = $b["category"] ?? "";
        $categoryComparison = strcasecmp($categoryA, $categoryB);

        // If categories are the same, sort by node name.
        if ($categoryComparison === 0) {
          $nameA = $a["name"] ?? "";
          $nameB = $b["name"] ?? "";
          return strcasecmp($nameA, $nameB);
        }

        return $categoryComparison;
      });

      return $this->cacheableSuccessResponse($nodes, $cacheMetadata);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to fetch node types: @message', [
        '@message' => $e->getMessage(),
      ]);
      return $this->cacheableErrorResponse('Failed to fetch node types', Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
    }
  }

  /**
   * Get node type metadata.
   *
   * @param string $node_type_id
   *   The node type ID.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with node type metadata.
   */
  public function getNodeMetadata(string $node_type_id): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);

    try {
      $entity = $this->entityTypeManager()->getStorage("flowdrop_node_type")->load($node_type_id);

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        // Cache the 404 response but invalidate if this entity is created.
        $cacheMetadata->addCacheTags(['config:flowdrop_node_type_list']);
        return $this->cacheableErrorResponse("Node type not found", Response::HTTP_NOT_FOUND, $cacheMetadata);
      }

      // Add cache dependency on the entity.
      $cacheMetadata->addCacheableDependency($entity);

      $nodeMetadata = $this->buildNodeData($entity, $cacheMetadata);
      if ($nodeMetadata === NULL) {
        return $this->cacheableErrorResponse("Failed to build node metadata", Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
      }

      return $this->cacheableSuccessResponse([
        "node_type_id" => $node_type_id,
        "metadata" => $nodeMetadata->toArray(),
      ], $cacheMetadata);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to fetch node metadata for @id: @message', [
        '@id' => $node_type_id,
        '@message' => $e->getMessage(),
      ]);
      return $this->cacheableErrorResponse('Failed to fetch node metadata', Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
    }
  }

  /**
   * Get node types by category.
   *
   * @param string $category
   *   The category to filter by (plural form from API).
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with filtered node types.
   */
  public function getNodesByCategory(string $category): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);
    // Add list cache tag - invalidated when any node type is added/removed.
    $cacheMetadata->addCacheTags(['config:flowdrop_node_type_list']);

    try {

      $storage = $this->entityTypeManager()->getStorage("flowdrop_node_type");
      $ids = $storage->getQuery()
        ->condition("category", $category)
        ->accessCheck(TRUE)
        ->execute();
      $entities = $storage->loadMultiple($ids);

      $nodes = [];
      foreach ($entities as $entity) {
        if ($entity instanceof FlowDropNodeTypeInterface && $entity->isEnabled()) {
          // Add cache dependency on each entity.
          $cacheMetadata->addCacheableDependency($entity);

          $nodeMetadata = $this->buildNodeData($entity, $cacheMetadata);
          if ($nodeMetadata !== NULL) {
            $nodes[] = $nodeMetadata->toArray();
          }
        }
      }

      return $this->cacheableSuccessResponse($nodes, $cacheMetadata);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to fetch nodes by category @category: @message', [
        '@category' => $category,
        '@message' => $e->getMessage(),
      ]);
      return $this->cacheableErrorResponse('Failed to fetch nodes by category', Response::HTTP_INTERNAL_SERVER_ERROR, $cacheMetadata);
    }
  }

  /**
   * Validate node type configuration.
   *
   * @param string $node_type_id
   *   The node type ID.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request object.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with validation results.
   */
  public function validateConfiguration(string $node_type_id, Request $request): JsonResponse {
    // Decode JSON outside try/catch so BadRequestHttpException propagates.
    $content = $this->decodeJsonRequest($request);

    try {
      $config = $content["config"] ?? [];

      $entity = $this->entityTypeManager()->getStorage("flowdrop_node_type")->load($node_type_id);

      if (!$entity instanceof FlowDropNodeTypeInterface) {
        return $this->errorResponse("Node type not found", Response::HTTP_NOT_FOUND);
      }

      $executorPlugin = $entity->getExecutorPlugin();
      $parameterSchema = $this->getPluginParameterSchema($executorPlugin);
      $entityParameters = $entity->getParameters();

      $errors = [];
      $properties = $parameterSchema["properties"] ?? [];

      foreach ($properties as $propertyName => $propertySchema) {
        $entityParam = $entityParameters[$propertyName] ?? [];

        // Flags from entity config with system defaults.
        $required = $entityParam["required"] ?? FALSE;
        $configurable = $entityParam["configurable"] ?? FALSE;

        // Only validate configurable parameters in config.
        if ($configurable && $required) {
          if (!array_key_exists($propertyName, $config)) {
            $errors[] = "Required property '{$propertyName}' is missing";
          }
        }

        if (array_key_exists($propertyName, $config)) {
          $value = $config[$propertyName];
          $expectedType = $propertySchema["type"] ?? "mixed";

          if (!$this->validateValueType($value, $expectedType)) {
            $errors[] = "Property '{$propertyName}' must be of type '{$expectedType}'";
          }
        }
      }

      return $this->successResponse([
        "node_type_id" => $node_type_id,
        "validation_result" => [
          "valid" => empty($errors),
          "errors" => $errors,
        ],
      ]);
    }
    catch (\Exception $e) {
      $this->getLogger('flowdrop_node_type')->error('Failed to validate configuration for @id: @message', [
        '@id' => $node_type_id,
        '@message' => $e->getMessage(),
      ]);
      return $this->errorResponse('Failed to validate configuration', Response::HTTP_INTERNAL_SERVER_ERROR);
    }
  }

  /**
   * Build complete node data by merging entity and plugin data.
   *
   * Uses the Unified Parameter System to derive inputSchema and configSchema
   * from the plugin's parameterSchema combined with entity configuration.
   *
   * Note: The visual type is now controlled by the config entity,
   * not the plugin.
   * This allows site builders to choose the appropriate visual representation.
   * When multiple supported types are configured, a "type" parameter is added
   * to the configSchema to allow per-instance type selection.
   *
   * @param \Drupal\flowdrop_node_type\FlowDropNodeTypeInterface $flowdropNodeType
   *   The node type entity.
   * @param \Drupal\Core\Cache\CacheableMetadata|null $cacheMetadata
   *   Optional cache metadata to add dependencies to.
   *
   * @return \Drupal\flowdrop\DTO\NodeMetadata\NodeMetadata|null
   *   The complete node metadata DTO or NULL if plugin not found.
   */
  protected function buildNodeData(FlowDropNodeTypeInterface $flowdropNodeType, ?CacheableMetadata $cacheMetadata = NULL): ?NodeMetadata {
    try {
      $executorPlugin = $flowdropNodeType->getExecutorPlugin();

      $inputs = [];
      $outputs = [];
      $configSchema = [];
      $configDefaults = [];

      if ($executorPlugin && $this->pluginManager->hasDefinition($executorPlugin)) {
        $nodeProcessor = $this->pluginManager->createInstance($executorPlugin);

        // Get unified parameter schema from plugin.
        $parameterSchema = $nodeProcessor->getParameterSchema();
        $outputSchema = $nodeProcessor->getOutputSchema();

        // Get entity configuration.
        $entityParameters = $flowdropNodeType->getParameters();
        $entityOutputs = $flowdropNodeType->getOutputs();

        // Derive inputSchema and configSchema
        // from parameterSchema + entity config.
        $derivedSchemas = $this->deriveSchemas($parameterSchema, $entityParameters);
        $inputSchema = $derivedSchemas["inputSchema"];
        $configSchema = $derivedSchemas["configSchema"];
        $configDefaults = $derivedSchemas["configDefaults"];

        // Always add trigger input if not present.
        if (empty($inputSchema["properties"][ReservedName::PORT_TRIGGER])) {
          $inputSchema["properties"][ReservedName::PORT_TRIGGER] = [
            "type" => "trigger",
            "title" => "Trigger",
            "required" => FALSE,
          ];
        }

        // Transform schemas to port arrays.
        if (!empty($inputSchema["properties"])) {
          $inputs = $this->transformSchemaToPorts($inputSchema, "input");
        }

        // Filter outputs based on entity configuration.
        $filteredOutputSchema = $this->filterOutputSchema($outputSchema, $entityOutputs);

        // Always add trigger output if not present.
        // This allows any node to emit a trigger signal for flow control.
        if (empty($filteredOutputSchema["properties"][ReservedName::PORT_TRIGGER])) {
          $filteredOutputSchema["properties"][ReservedName::PORT_TRIGGER] = [
            "type" => "trigger",
            "title" => "Trigger",
            "description" => "Emitted when this node completes execution",
          ];
        }

        if (!empty($filteredOutputSchema["properties"])) {
          $outputs = $this->transformSchemaToPorts($filteredOutputSchema, "output");
        }

        // Add unified "input" port if configured (reserved name).
        // Only includes keys that are connectable (respects site admin config).
        if ($flowdropNodeType->isUnifiedInputExposed()) {
          $connectableKeys = $this->getConnectableKeys($parameterSchema, $entityParameters);
          $keysDescription = !empty($connectableKeys)
            ? "Available keys: " . implode(", ", $connectableKeys)
            : "No connectable parameters available";

          array_unshift($inputs, new NodePort(
            id: ReservedName::PORT_INPUT,
            name: "Input",
            type: "input",
            dataType: "json",
            required: FALSE,
            description: "All inputs as JSON object. {$keysDescription}",
          ));
        }

        // Add unified "output" port if configured (reserved name).
        // Only includes keys that are exposed (respects site admin config).
        if ($flowdropNodeType->isUnifiedOutputExposed()) {
          $exposedKeys = $this->getExposedOutputKeys($outputSchema, $entityOutputs);
          $keysDescription = !empty($exposedKeys)
            ? "Available keys: " . implode(", ", $exposedKeys)
            : "No exposed outputs available";

          array_unshift($outputs, new NodePort(
            id: ReservedName::PORT_OUTPUT,
            name: "Output",
            type: "output",
            dataType: "json",
            required: FALSE,
            description: "All outputs as JSON object. {$keysDescription}",
          ));
        }
      }

      // Get visual type configuration from config entity.
      $visualType = $flowdropNodeType->getVisualType();
      $supportedTypes = $flowdropNodeType->getSupportedVisualTypes();

      // Ensure supportedTypes has at least the default type.
      if (empty($supportedTypes)) {
        $supportedTypes = [$visualType];
      }

      // If multiple types are supported, add a "type" parameter to configSchema
      // so users can change the visual type per node instance.
      if (count($supportedTypes) > 1) {
        $configSchema["properties"][ReservedName::NODE_TYPE] = [
          "type" => "string",
          "title" => "Visual Type",
          "description" => "Select the visual appearance for this node instance.",
          "enum" => $supportedTypes,
          "default" => $visualType,
        ];
        $configDefaults["type"] = $visualType;
      }

      // Add instance display override properties to all nodes.
      // These allow users to customize label/description per node instance.
      // Uses x-display-order to position at the top of the config form.
      // Skip if configSchema already defines these properties (Idea node).
      if (!isset($configSchema["properties"][ReservedName::INSTANCE_TITLE])) {
        $configSchema["properties"][ReservedName::INSTANCE_TITLE] = [
          "type" => "string",
          "title" => "Title",
          "description" => "Override the default node title for this instance.",
          "x-display-order" => -2,
        ];
      }
      if (!isset($configSchema["properties"][ReservedName::INSTANCE_DESCRIPTION])) {
        $configSchema["properties"][ReservedName::INSTANCE_DESCRIPTION] = [
          "type" => "string",
          "title" => "Description",
          "description" => "Override the default description for this instance.",
          "format" => "multiline",
          "x-display-order" => -1,
        ];
      }

      // Check if the plugin provides configEdit metadata for dynamic
      // configuration. This allows plugins to specify how their configuration
      // UI should be loaded (dynamic schema from API, external edit links,
      // or both). Uses ConfigEditProviderInterface for type-safe interface
      // checking.
      $configEdit = NULL;
      if (isset($nodeProcessor) && $nodeProcessor instanceof ConfigEditProviderInterface) {
        $configEdit = $nodeProcessor->getConfigEdit();
      }

      // Build NodeMetadata using the fluent builder.
      return NodeMetadataBuilder::create()
        ->setId((string) $flowdropNodeType->id())
        ->setName((string) $flowdropNodeType->label())
        ->setType($visualType)
        ->setSupportedTypes($supportedTypes)
        ->setDescription($flowdropNodeType->getDescription())
        ->setCategory($flowdropNodeType->getCategory())
        ->setIcon($flowdropNodeType->getIcon())
        ->setPluginVersion($flowdropNodeType->getPluginVersion())
        ->setEnabled($flowdropNodeType->isEnabled())
        ->setTags($flowdropNodeType->getTags())
        ->setExecutorPlugin($executorPlugin)
        ->setInputs($inputs)
        ->setOutputs($outputs)
        ->setConfig($configDefaults)
        ->setConfigSchema($configSchema)
        ->setConfigEdit($configEdit)
        ->build();
    }
    catch (\Exception $e) {
      $this->getLogger("flowdrop")->error("Failed to build node data for @id: @error", [
        "@id" => $flowdropNodeType->id(),
        "@error" => $e->getMessage(),
      ]);
      return NULL;
    }
  }

  /**
   * Derive inputSchema and configSchema from unified parameterSchema.
   *
   * This is the core of the Unified Parameter System's API layer:
   * - Parameters with connectable=TRUE go into inputSchema (input ports)
   * - Parameters with configurable=TRUE go into configSchema (config form)
   * - Default values are extracted for the config object.
   *
   * Flags are controlled entirely by entity configuration with system defaults:
   * - connectable: entity config, else FALSE
   * - configurable: entity config, else TRUE
   * - required: entity config, else FALSE
   *
   * @param array<string, mixed> $parameterSchema
   *   The plugin's parameter schema from getParameterSchema().
   *   Contains standard JSON Schema (type, default, enum, etc.).
   * @param array<string, array<string, mixed>> $entityParameters
   *   The entity's parameter configuration (controls port/form behavior).
   *
   * @return array<string, mixed>
   *   Array with 'inputSchema', 'configSchema', and 'configDefaults'.
   */
  protected function deriveSchemas(array $parameterSchema, array $entityParameters): array {
    $inputProperties = [];
    $configProperties = [];
    $configDefaults = [];
    $inputRequired = [];
    $configRequired = [];

    $properties = $parameterSchema["properties"] ?? [];

    foreach ($properties as $paramName => $paramSchema) {
      // Dynamic port definitions (dynamicInputs, dynamicOutputs) are ALWAYS:
      // 1. Routed to configSchema (never connectable as input ports)
      // 2. Treated as configurable (shown in the config form)
      // The UI uses these definitions to render the dynamic port manager.
      // Individual port VALUES are resolved separately by ParameterResolver.
      if (in_array($paramName, ReservedName::configSchemaOnlyParams(), TRUE)) {
        $cleanSchema = $this->cleanPropertySchema($paramSchema);
        $configProperties[$paramName] = $cleanSchema;
        $entityDefault = ($entityParameters[$paramName] ?? [])["default"] ?? NULL;
        $configDefaults[$paramName] = $entityDefault ?? $paramSchema["default"] ?? [];
        continue;
      }

      // Skip hidden parameters.
      // These are automatically injected by the runtime and should not
      // appear as inputs or config options in the UI.
      $format = $paramSchema["format"] ?? NULL;
      $isHidden = $format === "hidden" || ($paramSchema["hidden"] ?? FALSE) === TRUE;
      if ($isHidden) {
        continue;
      }

      // Get entity-level configuration for this parameter.
      $entityConfig = $entityParameters[$paramName] ?? [];

      // Determine flags from entity config with system defaults.
      // Plugins no longer declare these - entity has full control.
      $configurable = $entityConfig["configurable"] ?? FALSE;
      $connectable = $entityConfig["connectable"] ?? FALSE;
      $required = $entityConfig["required"] ?? FALSE;

      // Get default value (entity overrides schema default).
      $defaultValue = $entityConfig["default"] ?? $paramSchema["default"] ?? NULL;

      // Build clean property schema (without flowdrop metadata).
      $cleanSchema = $this->cleanPropertySchema($paramSchema);

      // Add to inputSchema if connectable (shows as input port).
      if ($connectable) {
        $inputProperties[$paramName] = $cleanSchema;
        if ($required && !$configurable) {
          // Only required via input if no config fallback exists.
          $inputRequired[] = $paramName;
        }
      }

      // Add to configSchema if configurable (shows in config form).
      if ($configurable) {
        $configProperties[$paramName] = $cleanSchema;
        if ($required && !$connectable) {
          // Only required via config if no input fallback exists.
          $configRequired[] = $paramName;
        }

        // Add default value to config object.
        if ($defaultValue !== NULL) {
          $configDefaults[$paramName] = $defaultValue;
        }
      }
    }

    return [
      "inputSchema" => [
        "type" => "object",
        "properties" => $inputProperties,
        "required" => $inputRequired,
      ],
      "configSchema" => [
        "type" => "object",
        "properties" => $configProperties,
        "required" => $configRequired,
      ],
      "configDefaults" => $configDefaults,
    ];
  }

  /**
   * Clean property schema for API response.
   *
   * Returns a copy of the schema suitable for API response.
   * Currently a pass-through since plugins no longer include
   * FlowDrop-specific metadata in their schemas.
   *
   * @param array<string, mixed> $schema
   *   The property schema.
   *
   * @return array<string, mixed>
   *   Clean schema suitable for API response.
   */
  protected function cleanPropertySchema(array $schema): array {
    return $schema;
  }

  /**
   * Filter output schema based on entity configuration.
   *
   * @param array<string, mixed> $outputSchema
   *   The plugin's output schema.
   * @param array<string, array<string, mixed>> $entityOutputs
   *   The entity's output configuration.
   *
   * @return array<string, mixed>
   *   Filtered output schema.
   */
  protected function filterOutputSchema(array $outputSchema, array $entityOutputs): array {
    $filteredProperties = [];
    $properties = $outputSchema["properties"] ?? [];

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

      // Default to exposed=true if not configured.
      $exposed = $entityConfig["exposed"] ?? TRUE;

      if ($exposed) {
        $filteredProperties[$outputName] = $outputDef;
      }
    }

    return [
      "type" => "object",
      "properties" => $filteredProperties,
    ];
  }

  /**
   * 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 {
      if ($this->pluginManager->hasDefinition($pluginId)) {
        $plugin = $this->pluginManager->createInstance($pluginId);
        return $plugin->getParameterSchema();
      }
    }
    catch (\Exception $e) {
      $this->getLogger("flowdrop")->error("Failed to get parameter schema for @id: @error", [
        "@id" => $pluginId,
        "@error" => $e->getMessage(),
      ]);
    }

    return [];
  }

  /**
   * Transform schema to NodePort array format.
   *
   * @param array<string, mixed> $schema
   *   The schema array.
   * @param string $portType
   *   The port type ('input' or 'output').
   *
   * @return array<\Drupal\flowdrop\DTO\NodeMetadata\NodePort>
   *   Array of NodePort objects.
   */
  protected function transformSchemaToPorts(array $schema, string $portType): array {
    $ports = [];
    $properties = $schema["properties"] ?? [];
    $requiredFields = $schema["required"] ?? [];

    foreach ($properties as $propertyName => $propertySchema) {
      // Extract the schema type, handling both string and array formats.
      // JSON Schema allows type to be an array (e.g., ["string", "null"]).
      $schemaType = $this->normalizeSchemaType($propertySchema["type"] ?? "string");

      $ports[] = new NodePort(
        id: $propertyName,
        name: $propertySchema["title"] ?? $propertyName,
        type: $portType,
        dataType: $this->mapSchemaTypeToDataType($schemaType),
        required: in_array($propertyName, $requiredFields, TRUE),
        description: $propertySchema["description"] ?? "",
        defaultValue: $propertySchema["default"] ?? NULL,
      );
    }

    return $ports;
  }

  /**
   * Normalize a JSON Schema type to a single string type.
   *
   * JSON Schema allows type to be either a string or an array of strings
   * (e.g., ["string", "null"] for nullable values). This method normalizes
   * the type to a single string by selecting the first non-null type.
   *
   * @param string|array<int, string> $type
   *   The JSON Schema type value (string or array of strings).
   *
   * @return string
   *   A normalized single type string.
   */
  protected function normalizeSchemaType(string|array $type): string {
    // If type is already a string, return it directly.
    if (is_string($type)) {
      return $type;
    }

    // If type is an array, find the first non-null type.
    // Common pattern: ["string", "null"] for nullable string.
    foreach ($type as $singleType) {
      if (is_string($singleType) && $singleType !== "null") {
        return $singleType;
      }
    }

    // Fallback to "string" if only "null" types or empty array.
    return "string";
  }

  /**
   * Map JSON schema types to NodeDataType.
   *
   * @param string $schemaType
   *   The JSON schema type.
   *
   * @return string
   *   The NodeDataType.
   */
  protected function mapSchemaTypeToDataType(string $schemaType): string {
    $typeMapping = [
      "string" => "string",
      "number" => "number",
      "integer" => "number",
      "boolean" => "boolean",
      "array" => "array",
      "object" => "json",
      "tool" => "tool",
      "mixed" => "mixed",
      "trigger" => "trigger",
      "branch" => "branch",
    ];

    return $typeMapping[$schemaType] ?? "string";
  }

  /**
   * Validate value type.
   *
   * @param mixed $value
   *   The value to validate.
   * @param string $expectedType
   *   The expected type.
   *
   * @return bool
   *   TRUE if the value matches the expected type.
   */
  protected function validateValueType(mixed $value, string $expectedType): bool {
    return match ($expectedType) {
      "string" => is_string($value),
      "integer" => is_int($value),
      "number" => is_float($value) || is_int($value),
      "boolean" => is_bool($value),
      "array" => is_array($value),
      "object" => is_array($value) || is_object($value),
      "mixed" => TRUE,
      default => TRUE,
    };
  }

  /**
   * Get parameter keys that are connectable.
   *
   * Used to determine which keys should be included in the unified "input"
   * port description and filtering.
   *
   * @param array<string, mixed> $parameterSchema
   *   The plugin's parameter schema.
   * @param array<string, array<string, mixed>> $entityParameters
   *   The entity's parameter configuration.
   *
   * @return array<string>
   *   Array of parameter keys that are connectable.
   */
  protected function getConnectableKeys(array $parameterSchema, array $entityParameters): array {
    $keys = [];
    $properties = $parameterSchema["properties"] ?? [];

    foreach ($properties as $name => $schema) {
      // Skip trigger - it has its own port.
      if ($name === ReservedName::PORT_TRIGGER) {
        continue;
      }

      // Skip hidden parameters.
      $format = $schema["format"] ?? NULL;
      $isHidden = $format === "hidden" || ($schema["hidden"] ?? FALSE) === TRUE;
      if ($isHidden) {
        continue;
      }

      $entityConfig = $entityParameters[$name] ?? [];
      $connectable = $entityConfig["connectable"] ?? FALSE;

      if ($connectable) {
        $keys[] = $name;
      }
    }

    return $keys;
  }

  /**
   * Get output keys that are exposed.
   *
   * Used to determine which keys should be included in the unified "output"
   * port description and filtering.
   *
   * @param array<string, mixed> $outputSchema
   *   The plugin's output schema.
   * @param array<string, array<string, mixed>> $entityOutputs
   *   The entity's output configuration.
   *
   * @return array<string>
   *   Array of output keys that are exposed.
   */
  protected function getExposedOutputKeys(array $outputSchema, array $entityOutputs): array {
    $keys = [];
    $properties = $outputSchema["properties"] ?? [];

    foreach ($properties as $name => $schema) {
      // Skip trigger - it has its own port.
      if ($name === ReservedName::PORT_TRIGGER) {
        continue;
      }

      $entityConfig = $entityOutputs[$name] ?? [];
      // Default to exposed=true if not configured.
      $exposed = $entityConfig["exposed"] ?? TRUE;

      if ($exposed) {
        $keys[] = $name;
      }
    }

    return $keys;
  }

  /**
   * Get port configuration for the FlowDrop system.
   *
   * @return \Drupal\Core\Cache\CacheableJsonResponse
   *   JSON response with port configuration.
   */
  public function getPortConfiguration(): CacheableJsonResponse {
    // Build cache metadata for the response.
    $cacheMetadata = new CacheableMetadata();
    $cacheMetadata->addCacheContexts([
      'user.permissions',
      'languages:language_interface',
    ]);
    $cacheMetadata->addCacheTags(['config:flowdrop_node_type.port_config']);

    $config = $this->config("flowdrop_node_type.port_config");

    $defaultConfig = [
      "version" => "1.0.0",
      "defaultDataType" => "string",
      "dataTypes" => [
        [
          "id" => "mixed",
          "name" => "Mixed Data",
          "description" => "Mixed data that can be bool, int, string, ...",
          "color" => "var(--fd-node-teal)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
        [
          "id" => "string",
          "name" => "String",
          "description" => "Text data",
          "color" => "var(--fd-node-emerald)",
          "category" => "basic",
          "enabled" => TRUE,
        ],
        [
          "id" => "number",
          "name" => "Number",
          "description" => "Numeric data",
          "color" => "var(--fd-node-blue)",
          "category" => "numeric",
          "enabled" => TRUE,
        ],
        [
          "id" => "boolean",
          "name" => "Boolean",
          "description" => "True/false values",
          "color" => "var(--fd-node-purple)",
          "category" => "logical",
          "enabled" => TRUE,
        ],
        [
          "id" => "array",
          "name" => "Array",
          "description" => "Ordered list of items",
          "color" => "var(--fd-node-amber)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "string[]",
          "name" => "String Array",
          "description" => "Array of strings",
          "color" => "var(--fd-node-emerald)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "number[]",
          "name" => "Number Array",
          "description" => "Array of numbers",
          "color" => "var(--fd-node-blue)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "boolean[]",
          "name" => "Boolean Array",
          "description" => "Array of true/false values",
          "color" => "var(--fd-node-purple)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "json[]",
          "name" => "JSON Array",
          "description" => "Array of JSON objects",
          "color" => "var(--fd-node-orange)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "file[]",
          "name" => "File Array",
          "description" => "Array of files",
          "color" => "var(--fd-node-red)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "image[]",
          "name" => "Image Array",
          "description" => "Array of images",
          "color" => "var(--fd-node-pink)",
          "category" => "collection",
          "enabled" => TRUE,
        ],
        [
          "id" => "json",
          "name" => "JSON",
          "description" => "JSON structured data",
          "color" => "var(--fd-node-orange)",
          "category" => "complex",
          "enabled" => TRUE,
        ],
        [
          "id" => "file",
          "name" => "File",
          "description" => "File data",
          "color" => "var(--fd-node-red)",
          "category" => "file",
          "enabled" => TRUE,
        ],
        [
          "id" => "image",
          "name" => "Image",
          "description" => "Image data",
          "color" => "var(--fd-node-pink)",
          "category" => "media",
          "enabled" => TRUE,
        ],
        [
          "id" => "audio",
          "name" => "Audio",
          "description" => "Audio data",
          "color" => "var(--fd-node-indigo)",
          "category" => "media",
          "enabled" => TRUE,
        ],
        [
          "id" => "video",
          "name" => "Video",
          "description" => "Video data",
          "color" => "var(--fd-node-teal)",
          "category" => "media",
          "enabled" => TRUE,
        ],
        [
          "id" => "url",
          "name" => "URL",
          "description" => "Web address",
          "color" => "var(--fd-node-cyan)",
          "category" => "special",
          "enabled" => TRUE,
        ],
        [
          "id" => "email",
          "name" => "Email",
          "description" => "Email address",
          "color" => "var(--fd-node-cyan)",
          "category" => "special",
          "enabled" => TRUE,
        ],
        [
          "id" => "date",
          "name" => "Date",
          "description" => "Date value",
          "color" => "var(--fd-node-lime)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
        [
          "id" => "datetime",
          "name" => "DateTime",
          "description" => "Date and time value",
          "color" => "var(--fd-node-lime)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
        [
          "id" => "time",
          "name" => "Time",
          "description" => "Time value",
          "color" => "var(--fd-node-lime)",
          "category" => "temporal",
          "enabled" => TRUE,
        ],
      ],
      "compatibilityRules" => [],
    ];

    $portConfig = $config->get("config") ?: $defaultConfig;

    return $this->cacheableSuccessResponse($portConfig, $cacheMetadata);
  }

}
