<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\flowdrop\DTO\ValidationResult;

/**
 * Validates a workflow definition's structural integrity.
 *
 * A single, transport-agnostic choke-point shared by every path that persists
 * a workflow (the entity form, the REST API) so they validate identically
 * rather than each re-implementing a subset. Operates on the raw structural
 * arrays — nodes, edges and exposure maps — so callers can validate a
 * prospective state before it is written, without an entity instance.
 *
 * The rules implemented here are the cheap, save-hot-path checks:
 * - R1: every node's declared executor plugin resolves to a real plugin.
 * - R4: exposure-map integrity (name format, per-side uniqueness, referenced
 *   node exists, referenced port is declared by the source plugin).
 * - R5: structural limits (node/edge counts, node id presence).
 * - R6: config validity — a node's user config (`data.config`) is validated
 *   per key against the config schema derived from the live node type + plugin
 *   (the single source of truth). Required config keys must be present; known
 *   keys must satisfy their type/enum/bound constraints (both block the save).
 *   Unknown keys are reported as warnings only — the runtime ignores any config
 *   key absent from the schema, so they cannot affect execution. The plugin's
 *   own `validateParams()` is deliberately NOT invoked here: it is a runtime
 *   contract over fully-resolved params (including connectable inputs supplied
 *   by edges), so calling it with authoring config alone would wrongly reject
 *   valid workflows.
 * - R7: every edge connects exposed ports only — a not-exposed port is not
 *   wireable, so an edge to/from one is rejected.
 * - R8: every edge endpoint resolves to an existing node. The editor removes
 *   connected edges on node deletion and the swap flow drops incompatible
 *   edges, so dangling edges only arise from hand-edited JSON, imports or
 *   migrations; this rejects them at save/import time. The read path
 *   ({@see \Drupal\flowdrop_workflow\DTO\WorkflowDTO::fromArray}) repairs the
 *   same condition by pruning, so already-stored corruption still opens.
 *
 * Schema-parity (comparing embedded node metadata against the live plugin
 * schema) is deliberately NOT done here — it instantiates plugins and walks
 * schemas, so it belongs to the import preflight and the diagnostics command,
 * not to every editor save.
 *
 * Errors and warnings are returned as a {@see ValidationResult}. Each item's
 * `parameter` is a locator the caller can route back to its own surface:
 * - `workflow` — a workflow-wide problem (R5 counts).
 * - `node.{id}` — a problem with a specific node (R1, missing id).
 * - `schema.{side}.{index}` — a problem with an exposure entry (R4), where
 *   `side` is `input` or `output` and `index` is the zero-based entry offset.
 * - `edge.{index}` — a problem with a specific edge (R7, R8), where `index` is
 *   the zero-based edge offset.
 * Each item's `code` is a stable machine code (the `CODE_*` constants).
 *
 * @api
 */
interface WorkflowValidatorInterface {

  /**
   * Maximum number of nodes a workflow may contain.
   */
  public const MAX_NODES = 500;

  /**
   * Maximum number of edges a workflow may contain.
   */
  public const MAX_EDGES = 1000;

  /**
   * R1: a node declares an executor plugin that does not resolve.
   */
  public const CODE_PLUGIN_MISSING = 'R1_PLUGIN_MISSING';

  /**
   * R4: an exposed name does not match the allowed pattern.
   */
  public const CODE_NAME_FORMAT = 'R4_NAME_FORMAT';

  /**
   * R4: an exposed name is used more than once on the same side.
   */
  public const CODE_NAME_DUPLICATE = 'R4_NAME_DUPLICATE';

  /**
   * R4: an exposure references a node that does not exist.
   */
  public const CODE_NODE_MISSING = 'R4_NODE_MISSING';

  /**
   * R4: an exposure references a port the source plugin does not declare.
   */
  public const CODE_PORT_MISSING = 'R4_PORT_MISSING';

  /**
   * R5: the workflow exceeds the maximum node count.
   */
  public const CODE_TOO_MANY_NODES = 'R5_TOO_MANY_NODES';

  /**
   * R5: the workflow exceeds the maximum edge count.
   */
  public const CODE_TOO_MANY_EDGES = 'R5_TOO_MANY_EDGES';

  /**
   * R5: a node has no identifier.
   */
  public const CODE_NODE_MISSING_ID = 'R5_NODE_MISSING_ID';

  /**
   * R6: a required config key is missing from a node's config.
   */
  public const CODE_CONFIG_REQUIRED = 'R6_CONFIG_REQUIRED';

  /**
   * R6: a config value violates its schema constraint (type/enum/bound).
   */
  public const CODE_CONFIG_INVALID = 'R6_CONFIG_INVALID';

  /**
   * R7: an edge lands on an input port that is not exposed on its target node.
   */
  public const CODE_EDGE_TARGET_NOT_EXPOSED = 'R7_EDGE_TARGET_NOT_EXPOSED';

  /**
   * R7: an edge emits from an output port that is not exposed on its source.
   */
  public const CODE_EDGE_SOURCE_NOT_EXPOSED = 'R7_EDGE_SOURCE_NOT_EXPOSED';

  /**
   * R8: an edge references a source node that no longer exists.
   */
  public const CODE_EDGE_SOURCE_MISSING = 'R8_EDGE_SOURCE_MISSING';

  /**
   * R8: an edge references a target node that no longer exists.
   */
  public const CODE_EDGE_TARGET_MISSING = 'R8_EDGE_TARGET_MISSING';

  /**
   * Validates a prospective workflow definition.
   *
   * @param array<int, array<string, mixed>> $nodes
   *   The workflow nodes, as stored on the entity.
   * @param array<int, array<string, mixed>> $edges
   *   The workflow edges, as stored on the entity.
   * @param list<array{name?: string, node_id?: string, port?: string}> $inputPorts
   *   The input exposure map.
   * @param list<array{name?: string, node_id?: string, port?: string}> $outputPorts
   *   The output exposure map.
   *
   * @return \Drupal\flowdrop\DTO\ValidationResult
   *   The validation result. Valid when there are no errors; warnings never
   *   block.
   */
  public function validate(array $nodes, array $edges, array $inputPorts, array $outputPorts): ValidationResult;

}
