<?php

declare(strict_types=1);

namespace Drupal\flowdrop\Utility;

use Drupal\flowdrop\Constants\ReservedName;

/**
 * Resolves a node port's effective exposure for a given instance.
 *
 * In v2 exposure is semantic, not cosmetic: a not-exposed port is hidden on the
 * canvas, not wireable, and not runtime-overridable (the author's config value
 * wins). The engine therefore must read exposure, and this is the single PHP
 * accessor it reads through — the mirror of fdnpm's `isPortExposed`
 * (libs/flowdrop/src/lib/utils/portUtils.ts).
 *
 * Effective exposure is the instance's explicit `data.config.ports` override
 * for the port, falling back to the port's metadata `exposedByDefault` (which
 * itself defaults to exposed). The instance override is the `ports` reserved
 * config key ({@see ReservedName::PORTS}), a per-direction ordered list of
 * `{id, exposed?}` entries; the `exposed` flag is present only when it diverges
 * from the default.
 *
 * Consumers: ParameterResolver (gates runtime/LLM override), ToolParameterScope
 * (gates the model-fillable set), WorkflowValidator (rejects edges to/from a
 * non-exposed port). Keep all exposure resolution here so the four
 * input-gathering paths can never drift.
 *
 * @api
 */
final class PortExposure {

  private function __construct() {}

  /**
   * Whether a port is exposed for an instance.
   *
   * @param array<string, mixed> $configPorts
   *   The instance's `data.config.ports` value (the {@see ReservedName::PORTS}
   *   key), shaped `{inputs: [{id, exposed?}], outputs: [{id, exposed?}]}`. An
   *   untouched node stores no `ports` key, so this is commonly an empty array.
   * @param string $direction
   *   The port direction: ReservedName::PORT_INPUT or ::PORT_OUTPUT.
   * @param string $portId
   *   The port id (for connectable params, the parameter name).
   * @param bool $exposedByDefault
   *   The port's metadata default exposure (NodePort::$exposedByDefault), used
   *   when the instance carries no explicit `exposed` flag for the port.
   *
   * @return bool
   *   TRUE when the port is exposed for this instance.
   */
  public static function isExposed(array $configPorts, string $direction, string $portId, bool $exposedByDefault = TRUE): bool {
    $key = $direction === ReservedName::PORT_OUTPUT ? 'outputs' : 'inputs';
    $entries = $configPorts[$key] ?? [];
    if (is_array($entries)) {
      foreach ($entries as $entry) {
        if (is_array($entry) && ($entry['id'] ?? NULL) === $portId && array_key_exists('exposed', $entry)) {
          return (bool) $entry['exposed'];
        }
      }
    }
    return $exposedByDefault;
  }

  /**
   * The metadata default exposure a port's schema declares.
   *
   * The single reader of the `x-exposed-by-default` schema extension: a port is
   * exposed by default unless its schema declares the flag *exactly* FALSE.
   * Every path that needs a port's default — NodeMetadataResolver building a
   * NodePort, ParameterResolver gating runtime override, ToolParameterScope
   * gating the model-fillable set — resolves it here so the coercion cannot
   * drift between them. The merge with the instance override is {@see
   * self::isExposed()}; this is only the default it falls back to.
   *
   * @param array<string, mixed> $portSchema
   *   The port/parameter's JSON-schema fragment (may be empty, e.g. a port the
   *   processor does not describe → exposed by default).
   *
   * @return bool
   *   FALSE only when the schema declares `x-exposed-by-default` exactly FALSE;
   *   TRUE otherwise.
   */
  public static function isExposedByDefault(array $portSchema): bool {
    return ($portSchema['x-exposed-by-default'] ?? TRUE) !== FALSE;
  }

}
