<?php

declare(strict_types=1);

namespace Drupal\flowdrop_workflow\Service;

use Drupal\flowdrop\Constants\EdgeKeys;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\RemoveEdge;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\RemoveNode;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\RemoveNodeConfigKey;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\ReplaceNodeType;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\SetNodeConfig;
use Drupal\flowdrop_workflow\Diagnostic\Mutation\WorkflowMutationInterface;
use Drupal\flowdrop_workflow\WorkflowDefinitionInterface;

/**
 * Default WorkflowMutator implementation.
 *
 * @internal
 */
final class WorkflowMutator implements WorkflowMutatorInterface {

  /**
   * {@inheritdoc}
   */
  public function apply(WorkflowDefinitionInterface $workflow, array $mutations): void {
    // Partition the mutations into removals (keyed for free dedup) and per-node
    // edits. A node removal supersedes any edit on the same node, so edits on
    // removed nodes are dropped below rather than applied then discarded.
    $removedNodes = [];
    $removedEdges = [];
    // Node id => list of SetNodeConfig|RemoveNodeConfigKey|ReplaceNodeType.
    $nodeEdits = [];

    foreach ($mutations as $mutation) {
      if ($mutation instanceof RemoveNode) {
        $removedNodes[$mutation->nodeId] = TRUE;
      }
      elseif ($mutation instanceof RemoveEdge) {
        $removedEdges[$mutation->edgeId] = TRUE;
      }
      elseif (
        $mutation instanceof SetNodeConfig
        || $mutation instanceof RemoveNodeConfigKey
        || $mutation instanceof ReplaceNodeType
      ) {
        $nodeEdits[$mutation->nodeId][] = $mutation;
      }
      else {
        throw new \InvalidArgumentException(sprintf(
          'Unknown workflow mutation: %s.',
          $mutation instanceof WorkflowMutationInterface ? $mutation::class : get_debug_type($mutation),
        ));
      }
    }

    $nodes = [];
    foreach ($workflow->getNodes() as $node) {
      if (!is_array($node)) {
        $nodes[] = $node;
        continue;
      }
      $id = (string) ($node['id'] ?? '');
      // A removed node supersedes any pending edit on it.
      if ($id !== '' && isset($removedNodes[$id])) {
        continue;
      }
      if ($id !== '' && isset($nodeEdits[$id])) {
        $node = $this->applyNodeEdits($node, $nodeEdits[$id]);
      }
      $nodes[] = $node;
    }

    $edges = [];
    foreach ($workflow->getEdges() as $index => $edge) {
      if (!is_array($edge)) {
        $edges[] = $edge;
        continue;
      }
      $id = (string) ($edge['id'] ?? '');
      $source = (string) ($edge[EdgeKeys::SOURCE] ?? '');
      $target = (string) ($edge[EdgeKeys::TARGET] ?? '');

      // Explicit removal by id, or — for an id-less hand-edited edge — by the
      // numeric index the diagnostic fell back to.
      $removeExplicit = ($id !== '' && isset($removedEdges[$id]))
        || ($id === '' && isset($removedEdges[(string) $index]));
      // Cascade: a node removal takes its connected edges with it, so the
      // result never carries a dangling edge.
      $cascade = isset($removedNodes[$source]) || isset($removedNodes[$target]);

      if ($removeExplicit || $cascade) {
        continue;
      }
      $edges[] = $edge;
    }

    $workflow->setNodes($nodes);
    $workflow->setEdges($edges);
  }

  /**
   * Applies the edits accumulated for a single node.
   *
   * @param array<string, mixed> $node
   *   The node to edit.
   * @param list<\Drupal\flowdrop_workflow\Diagnostic\Mutation\WorkflowMutationInterface> $edits
   *   The node-scoped edits, in submission order.
   *
   * @return array<string, mixed>
   *   The edited node.
   */
  private function applyNodeEdits(array $node, array $edits): array {
    $node['data'] = is_array($node['data'] ?? NULL) ? $node['data'] : [];

    foreach ($edits as $edit) {
      if ($edit instanceof ReplaceNodeType) {
        $node['data']['metadata'] = is_array($node['data']['metadata'] ?? NULL) ? $node['data']['metadata'] : [];
        $node['data']['metadata']['node_type_id'] = $edit->newNodeTypeId;
        continue;
      }

      $config = is_array($node['data']['config'] ?? NULL) ? $node['data']['config'] : [];
      if ($edit instanceof SetNodeConfig) {
        $config[$edit->key] = $edit->value;
      }
      elseif ($edit instanceof RemoveNodeConfigKey) {
        unset($config[$edit->key]);
      }
      $node['data']['config'] = $config;
    }

    return $node;
  }

}
