<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;

/**
 * Default WorkflowSchemaResolver implementation.
 *
 * @internal
 */
final class WorkflowSchemaResolver implements WorkflowSchemaResolverInterface {

  public function __construct(
    private readonly LoggerChannelInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  public function resolveInputs(
    WorkflowDefinitionInterface $workflow,
    array $namedInputs,
  ): array {
    $exposures = $this->indexByName($workflow->getInputPorts());
    $nodesById = $this->indexNodes($workflow->getNodes());
    $initialData = [];

    foreach ($namedInputs as $key => $value) {
      // Pass-through: callers already supplying node-keyed initialData
      // bypass the named-input translation.
      if (is_string($key) && isset($nodesById[$key]) && is_array($value)) {
        $initialData[$key] = isset($initialData[$key])
          ? array_merge($initialData[$key], $value)
          : $value;
        continue;
      }

      if (!is_string($key) || !isset($exposures[$key])) {
        // Unknown name — caller passed an input the workflow doesn't declare.
        // Drop it rather than corrupt initialData. Could be tightened to
        // throw in a future strict mode.
        continue;
      }

      $entry = $exposures[$key];
      $nodeId = $entry['node_id'] ?? '';
      $portName = $entry['port'] ?? '';
      if ($nodeId === '' || $portName === '' || !isset($nodesById[$nodeId])) {
        $this->logger->warning(
          'Named input @name dropped on workflow @workflow_id: node @node missing',
          [
            '@name' => $key,
            '@workflow_id' => $workflow->id() ?? '(new)',
            '@node' => $nodeId,
          ]
        );
        continue;
      }

      $initialData[$nodeId][$portName] = $value;
    }

    return $initialData;
  }

  /**
   * {@inheritdoc}
   */
  public function extractOutputs(
    WorkflowDefinitionInterface $workflow,
    array $results,
  ): array {
    $outputs = [];
    foreach ($workflow->getOutputPorts() as $entry) {
      $name = $entry['name'] ?? '';
      $nodeId = $entry['node_id'] ?? '';
      $portName = $entry['port'] ?? '';
      if ($name === '' || $nodeId === '' || $portName === '') {
        continue;
      }
      $nodeResults = $results[$nodeId] ?? NULL;
      if (!is_array($nodeResults) || !array_key_exists($portName, $nodeResults)) {
        $this->logger->warning(
          'Declared output @name on workflow @workflow_id: node @node port @port produced no value',
          [
            '@name' => $name,
            '@workflow_id' => $workflow->id() ?? '(new)',
            '@node' => $nodeId,
            '@port' => $portName,
          ]
        );
        continue;
      }
      $outputs[$name] = $nodeResults[$portName];
    }
    return $outputs;
  }

  /**
   * Indexes exposure entries by their name.
   *
   * @param list<array{name: string, node_id: string, port: string}> $entries
   *   The entries to index.
   *
   * @return array<string, array{name: string, node_id: string, port: string}>
   *   Entries keyed by name.
   */
  private function indexByName(array $entries): array {
    $indexed = [];
    foreach ($entries as $entry) {
      $name = $entry['name'] ?? '';
      if ($name !== '') {
        $indexed[$name] = $entry;
      }
    }
    return $indexed;
  }

  /**
   * Indexes nodes by their identifier.
   *
   * @param array<int, array<string, mixed>> $nodes
   *   The nodes to index.
   *
   * @return array<string, array<string, mixed>>
   *   Nodes keyed by identifier.
   */
  private function indexNodes(array $nodes): array {
    $indexed = [];
    foreach ($nodes as $node) {
      $id = $node['id'] ?? NULL;
      if (is_string($id) && $id !== '') {
        $indexed[$id] = $node;
      }
    }
    return $indexed;
  }

}
